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 <guido.casati@openairinterface.org>
This commit is contained in:
Guido Casati
2026-03-20 17:35:22 +01:00
parent 6786df3102
commit c8b99ce011
10 changed files with 248 additions and 106 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,