From c8b99ce011ad182f19ca59933af43b2a5dc312e8 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Fri, 20 Mar 2026 17:35:22 +0100 Subject: [PATCH] GTPU: move QFI handling to send path and update tunnel API usage Refactor GTP-U tunnel and send APIs so QFI is handled when sending packets, not stored in tunnel creation state. Update SDAP and CU-UP integration to use PDU-session keyed N3 mappings and explicit QFI-marked sends. This commit clarifies layering ownership: GTP-U stays transport-only (TEID lookup, decapsulation, extension parsing, callback dispatch), while SDAP owns QoS semantics (QFI handling, QoS-flow-to-DRB policy, default DRB behavior, and mapping updates); GTP-U does not perform runtime QFI-to-DRB mapping or synthesize QFI. Changes: - remove `outgoing_qfi` from `gtpv1u_gnb_create_tunnel_req_t` and stop storing QFI as tunnel creation metadata; `newGtpuCreateTunnel(...)` now carries only transport/tunnel identity parameters (incoming_bearer_id, outgoing_bearer_id, outgoing_teid, remote address, callbacks) - add `gtpv1uSendDirectWithQFI()` and pass QFI into `_gtpv1uSendDirect` to build UL PDU Session Container extensions - shift QFI handling from tunnel provisioning to per-packet TX APIs: QFI is passed explicitly only when sending (`gtpv1uSendDirectWithQFI(...)`) and is absent from non-SDAP/F1 sends (`gtpv1uSendDirect(...)`) - align N3 tunnel request semantics with session-level keys by setting incoming_rb_id to PDU session ID on N3 paths, while F1 paths keep DRB ID - keep `gtpv1uSendDirect()` and `gtpv1uSendDirectWithNRUSeqNum()` on `NO_QFI`, and enforce non-SDAP RX callback path only when QFI is absent - update `nr_sdap_rx_entity` to extract/validate QFI from SDAP UL headers, send UL data with `gtpv1uSendDirectWithQFI`, and use non-QFI send when SDAP header is disabled - add disabled-SDAP safety checks in SDAP entity setup/mapping to enforce single-DRB and single-flow constraints per PDU session - extend `test_gtp.cpp` with a `multi_qos_flows` scenario and QFI-aware send calls - update `nr-cuup-load-test.c` bearer setup fields and tunnel creation calls to match the new API - update tests/nr-cuup/nr-cuup-load-test.c to set explicit PDU session and QoS/SDAP parameters (sessionType, qosFlows[0], SDAP header flags), and to migrate both N3 and F1 tunnel creation calls to the new newGtpuCreateTunnel(...) signature (without outgoing_qfi) Signed-off-by: Guido Casati --- openair2/COMMON/gtpv1_u_messages_types.h | 5 +- .../LAYER2/NR_MAC_gNB/mac_rrc_dl_handler.c | 9 +- openair2/LAYER2/nr_pdcp/cucp_cuup_handler.c | 22 +---- openair2/RRC/NR/rrc_gNB_nsa.c | 1 - openair2/SDAP/nr_sdap/nr_sdap.c | 25 +++--- openair2/SDAP/nr_sdap/nr_sdap_entity.c | 64 ++++++++++--- openair3/ocp-gtpu/gtp_itf.cpp | 90 ++++++++++++------- openair3/ocp-gtpu/gtp_itf.h | 7 +- openair3/ocp-gtpu/tests/test_gtp.cpp | 90 ++++++++++++++++++- tests/nr-cuup/nr-cuup-load-test.c | 41 +++++++-- 10 files changed, 248 insertions(+), 106 deletions(-) diff --git a/openair2/COMMON/gtpv1_u_messages_types.h b/openair2/COMMON/gtpv1_u_messages_types.h index d5ba62a04d..a0d3433f65 100644 --- a/openair2/COMMON/gtpv1_u_messages_types.h +++ b/openair2/COMMON/gtpv1_u_messages_types.h @@ -143,11 +143,10 @@ typedef struct gtpv1u_gnb_create_tunnel_req_s { ue_id_t ue_id; // Outgoing TEID for the GTP tunnel teid_t outgoing_teid; - // Outgoing QFI - int outgoing_qfi; // PDU Session ID (1..255) identifies the PDU session int pdusession_id; - // Incoming RB + // Incoming TEID maps to this bearer ID: + // for F1-U, DRB ID; for N3-U, PDU session ID (tunnel key). uint16_t incoming_rb_id; // Destination TL address transport_layer_addr_t dst_addr; diff --git a/openair2/LAYER2/NR_MAC_gNB/mac_rrc_dl_handler.c b/openair2/LAYER2/NR_MAC_gNB/mac_rrc_dl_handler.c index 21b5afcf9a..9e31e2712d 100644 --- a/openair2/LAYER2/NR_MAC_gNB/mac_rrc_dl_handler.c +++ b/openair2/LAYER2/NR_MAC_gNB/mac_rrc_dl_handler.c @@ -63,12 +63,7 @@ static f1ap_up_tnl_t f1_drb_gtpu_create(const gtpv1u_gnb_create_tunnel_req_t *re { f1ap_up_tnl_t out = {0}; - LOG_I(GTPU, - "Incoming DRB %d / PDU Session %d - UL TEID %d QFI %d\n", - req->incoming_rb_id, - req->pdusession_id, - req->outgoing_teid, - req->outgoing_qfi); + LOG_I(GTPU, "Incoming DRB %d / PDU Session %d - UL TEID %d\n", req->incoming_rb_id, req->pdusession_id, req->outgoing_teid); instance_t f1inst = get_f1_gtp_instance(); DevAssert(f1inst >= 0); @@ -335,12 +330,10 @@ static int handle_ue_context_drbs_setup(NR_UE_info_t *UE, // just put same number of tunnels in DL as in UL DevAssert(drb->up_ul_tnl_len == 1); resp_drb->up_dl_tnl_len = drb->up_ul_tnl_len; - if (f1inst >= 0) { // we actually use F1-U // F1-U tunnel setup: 1 GTP-U tunnel per DRB gtpv1u_gnb_create_tunnel_req_t req = {.ue_id = UE->rnti, .outgoing_teid = drb->up_ul_tnl[0].teid, - .outgoing_qfi = -1, // don't put PDU session marker in F1 GTP .pdusession_id = drb->id, .incoming_rb_id = drb->id, .dst_addr.length = 32}; diff --git a/openair2/LAYER2/nr_pdcp/cucp_cuup_handler.c b/openair2/LAYER2/nr_pdcp/cucp_cuup_handler.c index ba58bbfb68..f1f6ca66cc 100644 --- a/openair2/LAYER2/nr_pdcp/cucp_cuup_handler.c +++ b/openair2/LAYER2/nr_pdcp/cucp_cuup_handler.c @@ -93,12 +93,7 @@ static UP_TL_information_t f1_drb_gtpu_create(const instance_t f1inst, { UP_TL_information_t out = {0}; - LOG_I(GTPU, - "Incoming DRB %d / PDU Session %d - UL TEID %d QFI %d\n", - req->incoming_rb_id, - req->pdusession_id, - req->outgoing_teid, - req->outgoing_qfi); + LOG_I(GTPU, "Incoming DRB %d / PDU Session %d - UL TEID %d\n", req->incoming_rb_id, req->pdusession_id, req->outgoing_teid); gtpv1u_gnb_create_tunnel_resp_t resp = {0}; int ret = gtpv1u_create_ngu_tunnel(f1inst, req, &resp, cu_f1u_data_req, NULL); @@ -126,12 +121,7 @@ static UP_TL_information_t n3_gtpu_create(const gtpv1u_gnb_create_tunnel_req_t * instance_t n3inst = get_n3_gtp_instance(); UP_TL_information_t out = {0}; - LOG_I(GTPU, - "Incoming DRB %d / PDU Session %d - UL TEID %d QFI %d\n", - req->incoming_rb_id, - req->pdusession_id, - req->outgoing_teid, - req->outgoing_qfi); + LOG_I(GTPU, "N3 GTP-U tunnel: PDUSession=%d/UL TEID=0x%08x\n", req->pdusession_id, req->outgoing_teid); gtpv1u_gnb_create_tunnel_resp_t resp = {0}; int ret = gtpv1u_create_ngu_tunnel(n3inst, req, &resp, nr_pdcp_data_req_drb, sdap_data_req); @@ -257,7 +247,6 @@ void e1_bearer_context_setup(const e1ap_bearer_setup_req_t *req) teid_t dummy_teid = 0xffff; // we will update later with answer from DU in_addr_t dummy_address = {0}; // IPv4, updated later with answer from DU gtpv1u_gnb_create_tunnel_req_t f1_tunnel_req = {.incoming_rb_id = req_drb->id, - .outgoing_qfi = -1, // don't put PDU session marker in GTP .outgoing_teid = dummy_teid, .pdusession_id = req_drb->id, .ue_id = cu_up_ue_id, @@ -282,16 +271,13 @@ void e1_bearer_context_setup(const e1ap_bearer_setup_req_t *req) e1_add_bearers(cu_up_ue_id, &DRB_configList, &security_parameters); ASN_STRUCT_RESET(asn_DEF_NR_DRB_ToAddModList, &DRB_configList.list); - /** GTP tunnel for N3/to core: one GTP-U tunnel per PDU session - * which can contains multiple QoS Flows - * @note Mapping only the default QFI (by implementation) */ + /** GTP tunnel for N3/to core: one GTP-U tunnel per PDU session. */ LOG_D(E1AP, "In %s: GTP tunnel for N3/to core for PDU Session %ld\n", __func__, req_pdu->sessionId); // N3 GTP-U Tunnel Request for PDU Session gtpv1u_gnb_create_tunnel_req_t n3_tunnel_req = {.ue_id = cu_up_ue_id, .outgoing_teid = req_pdu->UP_TL_information.teId, - .outgoing_qfi = req_pdu->DRBnGRanList[0].qosFlows[0].qfi, // one QFI, use as marking .pdusession_id = req_pdu->sessionId, - .incoming_rb_id = req_pdu->DRBnGRanList[0].id, + .incoming_rb_id = req_pdu->sessionId, .dst_addr.length = 32}; memcpy(&n3_tunnel_req.dst_addr.buffer, &req_pdu->UP_TL_information.tlAddress, sizeof(uint8_t) * 4); // only IPv4 now resp_pdu->tl_info = n3_gtpu_create(&n3_tunnel_req); diff --git a/openair2/RRC/NR/rrc_gNB_nsa.c b/openair2/RRC/NR/rrc_gNB_nsa.c index 9490eb22cd..1e5a56682a 100644 --- a/openair2/RRC/NR/rrc_gNB_nsa.c +++ b/openair2/RRC/NR/rrc_gNB_nsa.c @@ -333,7 +333,6 @@ void rrc_add_nsa_user(gNB_RRC_INST *rrc, x2ap_ENDC_sgnb_addition_req_t *m, sctp_ .incoming_rb_id = drb_id, .pdusession_id = drb_id, .outgoing_teid = 0xffff, // will be updated later - .outgoing_qfi = -1, // don't put PDU session marker in F1 GTP .dst_addr.length = 32, }; gtpv1u_gnb_create_tunnel_resp_t resp = {0}; diff --git a/openair2/SDAP/nr_sdap/nr_sdap.c b/openair2/SDAP/nr_sdap/nr_sdap.c index c78fdca973..69726072c3 100644 --- a/openair2/SDAP/nr_sdap/nr_sdap.c +++ b/openair2/SDAP/nr_sdap/nr_sdap.c @@ -48,19 +48,18 @@ bool sdap_data_req(protocol_ctxt_t *ctxt_p, return 0; } - bool ret = sdap_entity->tx_entity(sdap_entity, - ctxt_p, - srb_flag, - mui, - confirm, - sdu_buffer_size, - sdu_buffer, - pt_mode, - sourceL2Id, - destinationL2Id, - qfi, - rqi); - return ret; + return sdap_entity->tx_entity(sdap_entity, + ctxt_p, + srb_flag, + mui, + confirm, + sdu_buffer_size, + sdu_buffer, + pt_mode, + sourceL2Id, + destinationL2Id, + qfi, + rqi); } void sdap_data_ind(int pdcp_entity, int is_gnb, int pdusession_id, ue_id_t ue_id, char *buf, int size) diff --git a/openair2/SDAP/nr_sdap/nr_sdap_entity.c b/openair2/SDAP/nr_sdap/nr_sdap_entity.c index a0c5b6b075..3346874c68 100644 --- a/openair2/SDAP/nr_sdap/nr_sdap_entity.c +++ b/openair2/SDAP/nr_sdap/nr_sdap_entity.c @@ -191,13 +191,16 @@ static void nr_sdap_rx_entity(nr_sdap_entity_t *entity, char *buf, int size) { - /* The offset of the SDAP header, it might be 0 if has_sdap_rx is not true in the pdcp entity. */ - int offset=0; bool sdap_ul_rx = false; bool sdap_dl_rx = false; + int qfi = -1; /* If SDAP header is disabled for this entity, bypass header parsing */ if (entity->enable_sdap) { - uint8_t qfi = buf[0] & 0x3F; // QFI is always the first 6 bits in the first octet + /** Extract QFI from SDAP header for UL direction + * Per TS 37.324 §6.2.2, QFI is carried in SDAP header when SDAP header is present + * This QFI will be used in GTP-U extension header (PDU Session Container) + * for N3-U tunnel (the first 6 bits in the first octet) */ + qfi = buf[0] & 0x3F; if (qfi >= SDAP_MAX_QFI) { LOG_E(SDAP, "Invalid QFI %d received in SDAP header\n", qfi); return; @@ -209,10 +212,14 @@ static void nr_sdap_rx_entity(nr_sdap_entity_t *entity, if (is_gnb) { // gNB + /** QFI extracted from SDAP header for UL direction (N3-U tunnel) + * QFI range: 0..63 (6 bits) */ if (sdap_ul_rx) { // UL Data/Control PDU with SDAP header - offset = SDAP_HDR_LENGTH; + DevAssert(qfi >= 0 && qfi < SDAP_MAX_QFI); + int offset = SDAP_HDR_LENGTH; nr_sdap_ul_hdr_t *sdap_hdr = (nr_sdap_ul_hdr_t *)buf; - LOG_D(SDAP, "RX Entity Received QFI: %u\n", sdap_hdr->QFI); + DevAssert(sdap_hdr->QFI == qfi); + LOG_D(SDAP, "RX Entity Received QFI: %u\n", qfi); LOG_D(SDAP, "RX Entity Received R bit: %u\n", sdap_hdr->R); LOG_D(SDAP, "RX Entity Received DC bit: %u\n", sdap_hdr->DC); @@ -225,22 +232,32 @@ static void nr_sdap_rx_entity(nr_sdap_entity_t *entity, LOG_D(SDAP, "RX Entity Received SDAP Control PDU\n"); break; } + // Pushing SDAP SDU to GTP-U Layer + size_t gtp_len = size - offset; + uint8_t *gtp_buf = (uint8_t *)buf + offset; + LOG_D(SDAP, + "UL SDAP->GTP: ue=%lu pdu_session=%d qfi=%u payload_len=%zu\n", + ue_id, + pdusession_id, + qfi, + gtp_len); + /** Send to GTP-U with QFI marking for N3-U tunnel + * Per TS 29.281 §5.2, QFI is carried in PDU Session Container extension header + * For N3-U tunnels (qfi>=0), QFI is added to extension header for UL routing at UPF */ + gtpv1uSendDirectWithQFI(*N3GTPUInst, ue_id, pdusession_id, qfi, gtp_buf, gtp_len); + } else { + /* N3-U tunnel with no SDAP header: no QFI marking */ + DevAssert(qfi == -1); + LOG_D(SDAP, "UL SDAP->GTP: ue=%lu pdu_session=%d payload_len=%d\n", ue_id, pdusession_id, size); + gtpv1uSendDirect(*N3GTPUInst, ue_id, pdusession_id, (uint8_t *)buf, size, false, false); } - - uint8_t *gtp_buf = (uint8_t *)(buf + offset); - size_t gtp_len = size - offset; - - // Pushing SDAP SDU to GTP-U Layer - LOG_D(SDAP, "sending message to gtp size %ld\n", gtp_len); - // very very dirty hack gloabl var N3GTPUInst - instance_t inst = *N3GTPUInst; - gtpv1uSendDirect(inst, ue_id, pdusession_id, gtp_buf, gtp_len, false, false); } else { //nrUE /* * TS 37.324 5.2 Data transfer * 5.2.2 Downlink * if the DRB from which this SDAP data PDU is received is configured by RRC with the presence of SDAP header. */ + int offset = 0; if (sdap_dl_rx) { // DL Data/Control PDU with SDAP header offset = SDAP_HDR_LENGTH; /* @@ -461,6 +478,19 @@ static void nr_sdap_rm_qos_flows_from_drb(nr_sdap_entity_t *entity, const sdap_c * @param drb the DRB ID to be mapped */ static void nr_sdap_qfi2drb_map_update(nr_sdap_entity_t *entity, const sdap_config_t *sdap) { + /* disabled SDAP: only one DRB per PDU session existing qfi2drb_table entries must match the DRB to be updated */ + if (!entity->enable_sdap) { + for (int i = 0; i < SDAP_MAX_QFI; i++) { + int mapped = entity->qfi2drb_table[i].drb_id; + if (mapped != SDAP_NO_MAPPING_RULE) { + AssertFatal(mapped == sdap->drb_id, + "PDU session %d: disabled SDAP allows only one DRB per PDU session (qfi2drb_table[%d] maps to DRB %d)\n", + entity->pdusession_id, + i, + mapped); + } + } + } if (!entity->is_gnb) { // UE control PDU configuration nr_sdap_ue_control_pdu_config(entity, entity->ue_id, sdap); } @@ -507,6 +537,12 @@ static void nr_sdap_add_entity(const int is_gnb, const ue_id_t ue_id, const sdap LOG_I(SDAP, "Default DRB for the created SDAP entity: DRB %d \n", sdap_entity->default_drb); } + if (!sdap_entity->enable_sdap) { + AssertFatal(sdap->mappedQFIs2AddCount <= 1, + "PDU session %d: disabled SDAP allows at most one QoS flow to add at entity creation\n", + sdap->pdusession_id); + } + // Add QoS flows to the DRB (initial configuration) nr_sdap_add_qos_flows_to_drb(sdap_entity, sdap); diff --git a/openair3/ocp-gtpu/gtp_itf.cpp b/openair3/ocp-gtpu/gtp_itf.cpp index 568939f113..7e2a061387 100644 --- a/openair3/ocp-gtpu/gtp_itf.cpp +++ b/openair3/ocp-gtpu/gtp_itf.cpp @@ -109,7 +109,11 @@ typedef struct Gtpv1uExtHeader { #define GTP_END_MARKER (254) #define GTP_GPDU (255) -// GTP bearer context: for sending data +/** NO_QFI: indicates no QFI marking (F1-U tunnel or N3-U tunnel with no SDAP header) + * Used when there is no UL PDU Session Information (SDAP header) present */ +#define NO_QFI (-1) + +/** GTP bearer context: for sending data */ typedef struct gtpv1u_bearer_s { int sock_fd; struct sockaddr_storage ip; @@ -118,7 +122,6 @@ typedef struct gtpv1u_bearer_s { uint16_t seqNum; uint8_t npduNum; int32_t nru_sequence_number; - int outgoing_qfi; } gtpv1u_bearer_t; typedef struct { @@ -127,11 +130,15 @@ typedef struct { typedef struct { ue_id_t ue_id; - ebi_t incoming_rb_id; + /** Incoming TEID mapping key: + * - F1-U: DRB ID (direct TEID-to-DRB routing on non-SDAP callback path) + * - N3-U: PDU session ID (TEID-to-PDU session; SDAP callback then resolves QFI-to-DRB) */ + uint16_t incoming_rb_id; gtpCallback callBack; teid_t outgoing_teid; gtpCallbackSDAP callBackSDAP; - int pdusession_id; + /** PDU Session ID (1..255) */ + uint16_t pdusession_id; } ueidData_t; typedef struct { @@ -314,9 +321,13 @@ static int gtpv1uCreateAndSendMsg(gtpv1u_bearer_t *bearer, return !GTPNOK; } +/** Internal function to send GTP-U packet with optional QFI marking + * Per TS 29.281 §5.2, QFI is carried in PDU Session Container extension header for N3-U + * @param qfi QoS Flow Identifier (0..63) for N3-U, or NO_QFI (-1) for F1-U */ static void _gtpv1uSendDirect(instance_t instance, ue_id_t ue_id, int bearer_id, + int qfi, uint8_t *buf, size_t len, bool seqNumFlag, @@ -357,11 +368,10 @@ static void _gtpv1uSendDirect(instance_t instance, int extension_count = 0; gtpu_extension_header_t ext[2]; - if (bearer.outgoing_qfi != -1) { - /* 29.281 Figure 5.2.1-3 note 4 says PDU Session Container must come first. - * GTPU_EXT_UL_PDU_SESSION_INFORMATION is within a PDU Session Container - * so it must be put before any other extension. - */ + /** Add PDU Session Container extension header if QFI is present (N3-U tunnel) + * Per TS 29.281 Figure 5.2.1-3 note 4, PDU Session Container must be the first Extension Header + * Per TS 29.281 §5.2, QFI is carried in UL PDU Session Information IE for N3-U */ + if (qfi != NO_QFI) { ext[extension_count] = { .type = GTPU_EXT_UL_PDU_SESSION_INFORMATION, .ul_pdu_session_information = { @@ -371,10 +381,16 @@ static void _gtpv1uSendDirect(instance_t instance, .snp = false, .n3n9_delay_ind = false, .new_ie_flag = false, - .qfi = bearer.outgoing_qfi + .qfi = qfi, } }; extension_count++; + LOG_D(GTPU, + "UL TX: Adding PDU Session Container with QFI=%d (ue=%ld bearer_id=%d outgoing_teid=0x%x)\n", + qfi, + ue_id, + bearer_id, + bearer.teid_outgoing); } if (nru_seqnum != -1) { @@ -406,6 +422,21 @@ static void _gtpv1uSendDirect(instance_t instance, extension_count); } +/** Send GTP-U packet with QFI marking for N3-U tunnel + * Per TS 29.281 §5.2, QFI is carried in PDU Session Container extension header + * Used by SDAP layer when forwarding UL packets to N3-U tunnel + * @param qfi QoS Flow Identifier (0..63) extracted from SDAP header */ +void gtpv1uSendDirectWithQFI(instance_t instance, ue_id_t ue_id, int bearer_id, int qfi, uint8_t *buf, size_t len) +{ + AssertFatal(qfi >= 0 && qfi < MAX_QOS_FLOWS, + "Invalid QFI %d for gtpv1uSendDirectWithQFI (expected 0..%d)\n", + qfi, + MAX_QOS_FLOWS - 1); + _gtpv1uSendDirect(instance, ue_id, bearer_id, qfi, buf, len, false, false, -1); +} + +/** Send GTP-U packet with no QFI marking for F1-U tunnel + * @note qfi is set to NO_QFI (-1) for F1-U */ void gtpv1uSendDirect(instance_t instance, ue_id_t ue_id, int bearer_id, @@ -414,14 +445,7 @@ void gtpv1uSendDirect(instance_t instance, bool seqNumFlag, bool npduNumFlag) { - _gtpv1uSendDirect(instance, - ue_id, - bearer_id, - buf, - len, - seqNumFlag, - npduNumFlag, - -1); + _gtpv1uSendDirect(instance, ue_id, bearer_id, NO_QFI, buf, len, seqNumFlag, npduNumFlag, -1); } void gtpv1uSendDirectWithNRUSeqNum(instance_t instance, @@ -447,14 +471,7 @@ void gtpv1uSendDirectWithNRUSeqNum(instance_t instance, pthread_mutex_unlock(&globGtp.gtp_lock); - _gtpv1uSendDirect(instance, - ue_id, - bearer_id, - buf, - len, - false, - false, - nru_seqnum); + _gtpv1uSendDirect(instance, ue_id, bearer_id, NO_QFI, buf, len, false, false, nru_seqnum); } static void fillDlDeliveryStatusReport(gtpu_extension_header_t *ext, uint32_t RLC_buffer_availability, uint32_t NR_PDCP_PDU_SN) @@ -677,7 +694,6 @@ teid_t newGtpuCreateTunnel(instance_t instance, int incoming_bearer_id, int outgoing_bearer_id, teid_t outgoing_teid, - int outgoing_qfi, transport_layer_addr_t remoteAddr, gtpCallback callBack, gtpCallbackSDAP callBackSDAP) @@ -709,7 +725,6 @@ teid_t newGtpuCreateTunnel(instance_t instance, .sock_fd = (int) compatInst(instance), // avoid warning on narrowing conversion: instance is long, sock_fd is int .teid_incoming = incoming_teid, .teid_outgoing = outgoing_teid, - .outgoing_qfi = outgoing_qfi, }; int addrs_length_in_bytes = remoteAddr.length / 8; @@ -778,7 +793,6 @@ int gtpv1u_create_s1u_tunnel(instance_t instance, incoming_rb_id, create_tunnel_req->eps_bearer_id[i], create_tunnel_req->sgw_S1u_teid[i], - -1, // no pdu session in 4G create_tunnel_req->sgw_addr[i], callBack, NULL); @@ -858,7 +872,6 @@ int gtpv1u_create_ngu_tunnel(const instance_t instance, create_tunnel_req->incoming_rb_id, create_tunnel_req->pdusession_id, create_tunnel_req->outgoing_teid, - create_tunnel_req->outgoing_qfi, create_tunnel_req->dst_addr, callBack, callBackSDAP); @@ -1220,7 +1233,7 @@ static int Gtpv1uHandleGpdu(int h, uint8_t *msgBuf, uint32_t msgBufLen, const st // manyother attributes may come from create tunnel protocol_ctxt_t ctxt = { .enb_flag = 1, .rntiMaybeUEid = uedata.ue_id, }; const srb_flag_t srb_flag = SRB_FLAG_NO; - const rb_id_t rb_id = uedata.incoming_rb_id; + uint16_t rb_id = uedata.incoming_rb_id; const mui_t mui = RLC_MUI_UNDEFINED; const confirm_t confirm = RLC_SDU_CONFIRM_NO; const sdu_size_t sdu_buffer_size = msgBufLen - offset; @@ -1230,7 +1243,7 @@ static int Gtpv1uHandleGpdu(int h, uint8_t *msgBuf, uint32_t msgBufLen, const st const uint32_t destinationL2Id = 0; if (sdu_buffer_size > 0) { - if (qfi != -1 && uedata.callBackSDAP) { + if (qfi != NO_QFI && uedata.callBackSDAP) { if (!uedata.callBackSDAP(&ctxt, uedata.ue_id, srb_flag, @@ -1244,14 +1257,23 @@ static int Gtpv1uHandleGpdu(int h, uint8_t *msgBuf, uint32_t msgBufLen, const st qfi, rqi, uedata.pdusession_id)) - LOG_E(GTPU, "[%d] down layer refused incoming packet\n", h); + LOG_E(GTPU, "[%d] down layer refused incoming SDAP packet\n", h); } else { + /* Non-SDAP callback path: direct TEID-to-incoming_rb_id delivery via callBack. + * QFI must be absent on this path */ + AssertFatal(qfi == NO_QFI, + "[%d] Non-SDAP callback configured but QFI=%d is present (ue=%lu teid=0x%x)\n", + h, + qfi, + uedata.ue_id, + ntohl(msgHdr->teid)); if (!uedata.callBack(&ctxt, srb_flag, rb_id, mui, confirm, sdu_buffer_size, sdu_buffer, mode, &sourceL2Id, &destinationL2Id)) LOG_E(GTPU, "[%d] down layer refused incoming packet\n", h); } } - if (NR_PDCP_PDU_SN > 0 && NR_PDCP_PDU_SN % 5 == 0) { + /* Delivery status report path uses DRB-based RLC state: keep it on non-SDAP path only. */ + if (!uedata.callBackSDAP && NR_PDCP_PDU_SN > 0 && NR_PDCP_PDU_SN % 5 == 0) { LOG_D(GTPU, "Create and send DL DATA Delivery status for the previously received PDU, NR_PDCP_PDU_SN: %u \n", NR_PDCP_PDU_SN); int rlc_tx_buffer_space = nr_rlc_get_available_tx_space(ctxt.rntiMaybeUEid, rb_id + 3); LOG_D(GTPU, "Available buffer size in RLC for Tx: %d \n", rlc_tx_buffer_space); diff --git a/openair3/ocp-gtpu/gtp_itf.h b/openair3/ocp-gtpu/gtp_itf.h index fc4baffa25..d0db70c91d 100644 --- a/openair3/ocp-gtpu/gtp_itf.h +++ b/openair3/ocp-gtpu/gtp_itf.h @@ -110,9 +110,8 @@ typedef struct gtpv1u_gnb_delete_tunnel_req_s gtpv1u_gnb_delete_tunnel_req_t; teid_t newGtpuCreateTunnel(instance_t instance, ue_id_t ue_id, int incoming_bearer_id, - int outgoing_rb_id, - teid_t teid, - int outgoing_qfi, + int outgoing_bearer_id, + teid_t outgoing_teid, transport_layer_addr_t remoteAddr, gtpCallback callBack, gtpCallbackSDAP callBackSDAP); @@ -127,6 +126,8 @@ typedef struct gtpv1u_gnb_delete_tunnel_req_s gtpv1u_gnb_delete_tunnel_req_t; int newGtpuDeleteAllTunnels(instance_t instance, ue_id_t ue_id); void gtpv1uSendDirect(instance_t instance, ue_id_t ue_id, int bearer_id, uint8_t *buf, size_t len, bool seqNumFlag, bool npduNumFlag); + void gtpv1uSendDirectWithQFI(instance_t instance, ue_id_t ue_id, int bearer_id, int qfi, uint8_t *buf, size_t len); + void gtpv1uSendDirectWithNRUSeqNum(instance_t instance, ue_id_t ue_id, int bearer_id, diff --git a/openair3/ocp-gtpu/tests/test_gtp.cpp b/openair3/ocp-gtpu/tests/test_gtp.cpp index 6acc893112..37edd8e535 100644 --- a/openair3/ocp-gtpu/tests/test_gtp.cpp +++ b/openair3/ocp-gtpu/tests/test_gtp.cpp @@ -83,13 +83,13 @@ static void run_basic_test(uint32_t ue_id, * don't provide an address yet, hence "null_addr". Install the callback * specific to this test. */ transport_layer_addr_t null_addr = {.length = 32}; - teid_t t1 = newGtpuCreateTunnel(ep1, ue_id, pdu_id, pdu_id, -1, qfi, null_addr, callBack, callBackSDAP); + teid_t t1 = newGtpuCreateTunnel(ep1, ue_id, pdu_id, pdu_id, -1, null_addr, callBack, callBackSDAP); /* Create the sending end on ep2. We have ep1's address/TEID, so create the * remote endpoint. Don't provide a callback, as this is supposed to be * unidirectional. */ transport_layer_addr_t tl_addr1 = get_tl_addr(AF_INET, ip1); - teid_t t2 = newGtpuCreateTunnel(ep2, ue_id, pdu_id, pdu_id, t1, qfi, tl_addr1, NULL, NULL); + teid_t t2 = newGtpuCreateTunnel(ep2, ue_id, pdu_id, pdu_id, t1, tl_addr1, NULL, NULL); EXPECT_NE(t1, t2); // cannot be the same TEIDs @@ -105,7 +105,10 @@ static void run_basic_test(uint32_t ue_id, for (int i = 0; i < num_send; ++i) { for (size_t p = 0; p < sizeof(buf); p++) buf[p] = payload_counter++; - gtpv1uSendDirect(ep2, ue_id, pdu_id, buf, sizeof(buf), false, false); + if (qfi >= 0) + gtpv1uSendDirectWithQFI(ep2, ue_id, pdu_id, qfi, buf, sizeof(buf)); + else + gtpv1uSendDirect(ep2, ue_id, pdu_id, buf, sizeof(buf), false, false); } usleep(100 * 1000); // wait 100ms to give time to receive packets @@ -151,6 +154,77 @@ static bool recv_basic_conn_qfi(protocol_ctxt_t *ctxt, return true; } +static int recv_count_multi_qfi = 0; +static const uint8_t expected_qfis_multi_qfi[] = {1, 5, 9, 5, 1}; +static const int expected_qfis_multi_qfi_count = sizeof(expected_qfis_multi_qfi) / sizeof(expected_qfis_multi_qfi[0]); +static bool recv_multi_qfi_same_pdu(protocol_ctxt_t *ctxt, + const ue_id_t ue_id, + const srb_flag_t flag, + const mui_t mui, + const confirm_t confirm, + const sdu_size_t size, + unsigned char *const buf, + const pdcp_transmission_mode_t modeP, + const uint32_t *sourceL2Id, + const uint32_t *destinationL2Id, + const uint8_t qfi, + const bool rqi, + const int pdusession_id) +{ + EXPECT_EQ(ue_id, 7U); + EXPECT_EQ(pdusession_id, 9); + EXPECT_LT(recv_count_multi_qfi, expected_qfis_multi_qfi_count); + EXPECT_EQ(qfi, expected_qfis_multi_qfi[recv_count_multi_qfi]); + EXPECT_EQ(size, 3); + for (int i = 0; i < size; ++i) + EXPECT_EQ(buf[i], recv_count_multi_qfi * size + i); + recv_count_multi_qfi++; + return true; +} + +static void run_multi_qos_flows_test(uint32_t ue_id, long pdu_id, const uint8_t *qfis, int num_send) +{ + /* set up two instances on different IPs */ + const char *ip1 = "127.0.0.1"; + const char *ip2 = "127.0.0.2"; + uint16_t port = 4567; + instance_t ep1 = init_gtp(ip1, port); + EXPECT_GE(ep1, 1); + instance_t ep2 = init_gtp(ip2, port); + EXPECT_GE(ep2, 1); + EXPECT_NE(ep1, ep2); + + transport_layer_addr_t null_addr = {.length = 32}; + teid_t t1 = newGtpuCreateTunnel(ep1, ue_id, pdu_id, pdu_id, -1, null_addr, NULL, recv_multi_qfi_same_pdu); + + transport_layer_addr_t tl_addr1 = get_tl_addr(AF_INET, ip1); + teid_t t2 = newGtpuCreateTunnel(ep2, ue_id, pdu_id, pdu_id, t1, tl_addr1, NULL, NULL); + EXPECT_NE(t1, t2); + + in_addr_t addr2 = get_addr(AF_INET, ip2); + GtpuUpdateTunnelOutgoingAddressAndTeid(ep1, ue_id, pdu_id, addr2, t2); + + uint8_t buf[3]; + uint8_t payload_counter = 0; + for (int i = 0; i < num_send; ++i) { + for (size_t p = 0; p < sizeof(buf); p++) + buf[p] = payload_counter++; + gtpv1uSendDirectWithQFI(ep2, ue_id, pdu_id, qfis[i], buf, sizeof(buf)); + } + + usleep(100 * 1000); + EXPECT_EQ(recv_count_multi_qfi, num_send); + + int ret = newGtpuDeleteAllTunnels(ep1, ue_id); + EXPECT_EQ(ret, 0); + ret = newGtpuDeleteAllTunnels(ep2, ue_id); + EXPECT_EQ(ret, 0); + ret = gtpv1Term(ep1); + EXPECT_EQ(ret, 0); + ret = gtpv1Term(ep2); + EXPECT_EQ(ret, 0); +} + /* Test unidirectional GTP message forwarding for a single UE with QFI. */ TEST(gtp, basic_conn_qfi) { @@ -162,6 +236,16 @@ TEST(gtp, basic_conn_qfi) run_basic_test(ue_id, pdu_id, qfi, num_send, &recv_count_qfi, NULL, recv_basic_conn_qfi); } +/* Test one PDU session carrying multiple QoS flows (different QFIs). */ +TEST(gtp, multi_qos_flows) +{ + uint32_t ue_id = 7; + long pdu_id = 9; + static const uint8_t qfis[] = {1, 5, 9, 5, 1}; + recv_count_multi_qfi = 0; + run_multi_qos_flows_test(ue_id, pdu_id, qfis, sizeof(qfis) / sizeof(qfis[0])); +} + static int recv_count = 0; static bool recv_basic_conn(protocol_ctxt_t *ctxt, const srb_flag_t srb_flagP, diff --git a/tests/nr-cuup/nr-cuup-load-test.c b/tests/nr-cuup/nr-cuup-load-test.c index b9ce7d6c4a..5597e2e439 100644 --- a/tests/nr-cuup/nr-cuup-load-test.c +++ b/tests/nr-cuup/nr-cuup-load-test.c @@ -17,6 +17,9 @@ configmodule_interface_t *uniqCfg; +/** QFI for NG-U GTP PDU Session Container */ +#define TEST_QOS_FLOW_ID 1 + #define DL 0 #define UL 1 @@ -101,6 +104,7 @@ static e1ap_bearer_setup_req_t get_breq(uint32_t ue_id, long pdu_id, long drb_id bearer_req.pduSession = calloc_or_fail(1, sizeof(*bearer_req.pduSession)); pdu_session_to_setup_t *pdu = &bearer_req.pduSession[0]; pdu->sessionId = pdu_id; + pdu->sessionType = E1AP_PDU_Session_Type_ipv4; pdu->nssai = *nssai; pdu->securityIndication.integrityProtectionIndication = SECURITY_NOT_NEEDED; pdu->securityIndication.confidentialityProtectionIndication = SECURITY_NOT_NEEDED; @@ -109,6 +113,18 @@ static e1ap_bearer_setup_req_t get_breq(uint32_t ue_id, long pdu_id, long drb_id DRB_nGRAN_to_setup_t *drb = &pdu->DRBnGRanList[0]; drb->id = drb_id; drb->numQosFlow2Setup = 1; + drb->qosFlows[0] = (qos_flow_to_setup_t){ + .qfi = TEST_QOS_FLOW_ID, + .qos_params.alloc_reten_priority.priority_level = E1AP_PriorityLevel_highest, + .qos_params.alloc_reten_priority.preemption_capability = E1AP_Pre_emptionCapability_shall_not_trigger_pre_emption, + .qos_params.alloc_reten_priority.preemption_vulnerability = E1AP_Pre_emptionVulnerability_not_pre_emptable, + .qos_params.qos_characteristics.qos_type = NON_DYNAMIC, + .qos_params.qos_characteristics.non_dynamic.fiveqi = 9, + }; + /* Enable SDAP headers: tests QFI-aware forwarding, so UL needs SDAP QFI marking, + * and DL payload received on F1-U includes SDAP+PDCP headers. */ + drb->sdap_config.sDAP_Header_UL = true; + drb->sdap_config.sDAP_Header_DL = true; drb->numCellGroups = 1; return bearer_req; @@ -181,9 +197,10 @@ static up_params_t setup_cuup_ue_ng(sctp_assoc_t assoc_id, const e1ap_nssai_t *n in_addr_t addr_lo; inet_pton(AF_INET, ip, &addr_lo); - /* create the local tunnel (i.e., as if we were UPF) */ + /* create the local tunnel (i.e., as if we were UPF) with N3-U bearer mappings */ transport_layer_addr_t null_addr = {.length = 32}; - teid_t teid_lo = newGtpuCreateTunnel(gtp_inst, ue_id, pdu_id, pdu_id, -1, -1, null_addr, NULL, recv_ng); + /* Intentionally disable non-SDAP callback on NG-U: this test must fail if packets arrive without QFI. */ + teid_t teid_lo = newGtpuCreateTunnel(gtp_inst, ue_id, pdu_id, pdu_id, -1, null_addr, NULL, recv_ng); UP_TL_information_t tnl = {.teId = teid_lo}; memcpy(&tnl.tlAddress, &addr_lo, 4); @@ -263,7 +280,8 @@ static bool recv_f1(protocol_ctxt_t *ctxt, const uint32_t *sourceL2Id, const uint32_t *destinationL2Id) { - int skip_bytes = 2; + /* F1-U DL payload contains PDCP (2 bytes) + SDAP (1 byte) header */ + int skip_bytes = 3; uint32_t *payload = (uint32_t *) &buf[skip_bytes]; DevAssert((size - skip_bytes) % 4 == 0); int ue_id = ctxt->rntiMaybeUEid; @@ -283,7 +301,7 @@ static void setup_cuup_ue_f1(sctp_assoc_t assoc_id, uint32_t ue_id, instance_t g /* create tunnel (i.e., as if we were DU) */ transport_layer_addr_t addr = {.length = 32}; memcpy(addr.buffer, &rm.tl_info.tlAddress, 4); - teid_t teid_lo = newGtpuCreateTunnel(gtp_inst, ue_id, drb_id, drb_id, rm.tl_info.teId, -1, addr, recv_f1, NULL); + teid_t teid_lo = newGtpuCreateTunnel(gtp_inst, ue_id, drb_id, drb_id, rm.tl_info.teId, addr, recv_f1, NULL); up_params_t lo = {.tl_info.teId = teid_lo, .cell_group_id = rm.cell_group_id}; inet_pton(AF_INET, ip, &lo.tl_info.tlAddress); @@ -408,18 +426,19 @@ static void *sender_thread(void *v) { struct thr_data *d = v; - // Overhead: PDCP header (2 bytes) for UL packets - // SDAP headers are disabled, so only PDCP header is added - size_t oh = d->is_ul ? 2 : 0; + // UL F1-U payload overhead: PDCP header (2 bytes) + SDAP header (1 byte) + size_t oh = d->is_ul ? 3 : 0; size_t packet_len = d->data_len + oh; uint8_t buf[packet_len]; memset(buf, 0, packet_len); + /* SDAP UL data PDU: bits[5:0]=QFI (TEST_QOS_FLOW_ID=1), bit6=R=0, bit7=DC=0 (data PDU) */ + buf[2] = TEST_QOS_FLOW_ID; uint32_t *payload = (uint32_t *)(buf + oh); uint64_t to_send = (float)d->throughput / 8.0 * d->duration; const float pps_1ms = (float) d->throughput / 8.0 / d->data_len / 1000; float pps_1ms_nextwin = pps_1ms; int bearer = d->is_ul ? 4 : 1; - + const int dl_qfi = TEST_QOS_FLOW_ID; //printf("sending %ld Mbps for %ld s => data %ld, %.3f pps\n", (float) d->throughput / 1000000.0, d->duration, to_send, pps_1ms); @@ -435,7 +454,11 @@ static void *sender_thread(void *v) if (d->is_ul) write_pdcp_header(packet_count, PDCP_SN_LEN_12, buf); *payload = packet_count++; - gtpv1uSendDirect(d->inst, d->ue_id, bearer, (uint8_t *)buf, packet_len, false, false); + if (d->is_ul) { // UL direction: F1-U tunnel + gtpv1uSendDirect(d->inst, d->ue_id, bearer, (uint8_t *)buf, packet_len, false, false); + } else { // DL direction: NG-U tunnel with SDAP enabled + gtpv1uSendDirectWithQFI(d->inst, d->ue_id, bearer, dl_qfi, (uint8_t *)buf, packet_len); + } d->total_data += d->data_len; }