NGAP interface architecture
13 minute read
Target audience: RAN engineers. Scope: the NGAP (N2) interface implementation in OCUDU — class ownership, procedure layout, per-procedure flows (interface mgmt, UE context, PDU session, NAS transport, paging, mobility), and how NGAP hands off into CU-CP / E1AP / F1AP / RRC. Reference: 3GPP TS 38.413. F1-U (NR-U user-plane) is data-plane and lives separately under lib/f1u/ — out of scope for this doc.
1. What NGAP Is in OCUDU
NGAP is the control-plane signaling protocol between the gNB and the 5GC AMF, carried over SCTP on the N2 reference point. In OCUDU, NGAP is a peer of CU-CP — not a subordinate — with its own state machine, ASN.1 codec layer, per-UE context store, and coroutine-based procedure framework. It sits in lib/ngap/ and is instantiated once per AMF association; a ngap_repository inside CU-CP manages the set.
Three design rules shape the module:
- One
ngap_implper AMF. A gNB can be connected to multiple AMFs (AMF pool per TS 23.501). Each AMF gets its own NGAP instance, its own TNL association, its own transaction managers, and its own served-GUAMI list. The CU-CP’sngap_repositorykeys them byamf_index_t. - Procedures are coroutines. Every request-response exchange (NG Setup, Initial Context Setup, HO Preparation, Path Switch, PDU Session Setup/Modify/Release, Context Release, Context Modification) is an
async_task<R>under lib/ngap/procedures/. Non-transaction messages (Initial UE Message, DL/UL NAS Transport, Paging, Error Indication) are handled by direct dispatch inngap_impl. - Handoff through
ngap_cu_cp_notifier. NGAP never reaches into CU-CP’s UE manager, RRC, F1AP, or E1AP directly. It calls a single notifier interface; the CU-CP-sidengap_cu_cp_adapterimplements that interface and orchestrates the rest of the stack.
2. Module Ownership Graph
ngap_impl (lib/ngap/ngap_impl.h) is the top-level class. It holds:
ngap_context_t context— global per-AMF state:gnb_id,ran_node_name,served_guami_list(set on NG Setup Response),supported_tas, procedure timeouts, PDU session request timeout, AMF name.ngap_ue_context_list ue_ctxt_list— UE context repository with three indices (byue_index,ran_ue_id,amf_ue_id) and a sequential RAN-UE-NGAP-ID allocator.ngap_transaction_manager ev_mng— global transaction event sources:ng_setup_outcome(Response/Failure),ng_reset_outcome(Ack).ngap_connection_handler conn_handler— wraps the N2 SCTP gateway client.tx_pdu_notifier_with_logging— outgoing PDU path; decorates the gateway notifier with PCAP/log.ngap_cu_cp_notifier& cu_cp_notifier— the only escape hatch into CU-CP; angap_metrics_aggregator,timer_manager&,task_executor& ctrl_exec.
Per-UE state — ngap_ue_context:
struct ngap_ue_ids { ue_index_t ue_index; ran_ue_id_t ran_ue_id; amf_ue_id_t amf_ue_id; };
struct ngap_ue_context {
ngap_ue_ids ue_ids;
ngap_cu_cp_ue_notifier* ue; // back to CU-CP UE
guami_t serving_guami;
unique_timer request_pdu_session_timer; // TS 38.413 tng-ranue-ngap
bool release_requested, release_scheduled;
std::optional<ngap_core_network_assist_info_for_inactive> core_network_assist_info_for_inactive;
byte_buffer last_pdu_session_resource_modify_request; // duplicate detect
ngap_ue_transaction_manager ev_mng; // HO Prep / Path Switch / HO Cancel / DL RAN Status
ngap_ue_logger logger;
};
The per-UE transaction manager is what lets an async_task coroutine await a specific response (e.g. HandoverCommand, PathSwitchRequestAck) with a timeout — the AMF’s matching PDU sets the event source, the coroutine resumes.
Factory. create_ngap(cfg, cu_cp_notifier, n2_gateway_handler, timers, ctrl_exec) instantiates one ngap_impl. The ngap_repository in CU-CP calls it once per AMF entry in the gNB config.
3. Interface Management Procedures (TS 38.413 Section 8.7)
sequenceDiagram
autonumber
participant CUCP as cu_cp
participant NGAP as ngap_impl
participant AMF
Note over CUCP,AMF: NG Setup (RAN-initiated, Section 8.7.1)
CUCP->>NGAP: handle_ng_setup_request(max_retries)
NGAP->>AMF: NGSetupRequest (gNB ID, TAs, PLMNs)
alt Success
AMF-->>NGAP: NGSetupResponse (served GUAMI, AMF name)
NGAP->>NGAP: ctx.served_guami_list = ...
else Failure w/ time_to_wait
AMF-->>NGAP: NGSetupFailure (time_to_wait)
NGAP->>NGAP: wait · retry (up to max_retries)
end
NGAP-->>CUCP: ng_setup_resultNG Setup (Section 8.7.1). procedures/ng_setup_procedure.{h,cpp}. Coroutine returning async_task<ngap_ng_setup_result>. Builds NGSetupRequest (global RAN node ID, supported TA list, PLMN/S-NSSAI lists), subscribes to ev_mng.ng_setup_outcome, sends, and awaits. On NGSetupFailure carrying a time_to_wait IE and retries remaining, it backs off and retries; otherwise returns failure. On success, the served-GUAMI list and AMF name are cached in ngap_context. This is the precondition for every other procedure on that N2 link.
NG Reset (Section 8.7.4). procedures/ng_reset_procedure.{h,cpp}. Coroutine returning async_task<void>. Supports both full reset and partial reset (UE list). Subscribes to ev_mng.ng_reset_outcome. On NGResetAcknowledge for a full reset, it cascades through ue_ctxt_list and tells CU-CP (via cu_cp_notifier) to release the affected UE contexts. A partial reset releases only the listed UEs.
Error Indication (Section 8.7.3). Asymmetric. Outgoing is a helper in ngap_error_indication_helper.h: send_error_indication(notifier, logger, [ran_ue_id], [amf_ue_id], [cause]). Can be UE-associated or non-UE-associated. Incoming is handled by ngap_impl::handle_error_indication() which logs the cause and does not cascade — Error Indication is informational in NGAP. A per-UE stored_error_indications map holds deferred indications that are attached to the next UEContextReleaseComplete.
AMF Configuration Update (Section 8.7.2), RAN Configuration Update, Overload Start/Stop (Section 8.7.5/6). Not currently implemented — these are optional for baseline interop. Overload gating, when implemented, will live on the CU-CP admission-control side rather than inside ngap_impl.
4. UE Context Management Procedures (TS 38.413 Section 8.3, Section 8.6)
Initial UE Message (Section 8.6.1). Handled directly by ngap_impl::handle_initial_ue_message(cu_cp_initial_ue_message). Triggered by CU-CP after the UE completes RRC Setup. Steps: allocate a fresh ran_ue_id_t via ue_ctxt_list.allocate_ran_ue_id(); create the ngap_ue_context; build InitialUEMessage with RAN-UE-ID, NAS PDU (from RRC), user location info (TAI + NR-CGI), GUAMI, establishment cause; start the request_pdu_session_timer (protects against AMF silence); send via tx_pdu_notifier. AMF-UE-NGAP-ID is not yet known here — it is bound later when AMF returns InitialContextSetupRequest.
Initial Context Setup (Section 8.3.1). procedures/ngap_initial_context_setup_procedure.{h,cpp}. AMF-initiated, request-response with failure option. On receipt:
- Extract AMF-UE-NGAP-ID; update
ue_ctxt_listso the index by AMF-UE-ID becomes populated. - Stop
request_pdu_session_timer. - Convert ASN.1 into
ngap_init_context_setup_request(security context, UE AMBR, PDU session list, GUAMI). CORO_AWAIT_VALUE(cu_cp_notifier.on_new_initial_context_setup_request(...))— this is the critical handoff: CU-CP runs RRC Security Mode Command → F1AP UE Context Setup → RRC Capability Transfer → (nested) PDU Session Resource Setup → RRC Reconfiguration.- On success, send
InitialContextSetupResponse(with PDU session admit list). On failure, sendInitialContextSetupFailurewith cause.
UE Context Release Request (RAN → AMF, Section 8.3.3). ngap_impl::handle_ue_context_release_request(). Triggered by CU-CP on RRC connection loss, inactivity, or internal error. Guards via release_requested flag to avoid duplicates. Encodes cause from ngap_cause_t (radio-network / transport / NAS / protocol / misc, per TS 38.413 Section 9.2.2.1). Scheduled on the per-UE executor, not inline, so the caller is not blocked.
UE Context Release Command / Complete (AMF → RAN, Section 8.3.2). procedures/ngap_ue_context_release_procedure.{h,cpp}. Coroutine. Sets release_scheduled, awaits cu_cp_notifier.on_new_ue_context_release_command(...) (which releases RRC, F1AP UE context, E1AP bearer context, and PDCP/SDAP/GTP-U resources), then sends UEContextReleaseComplete — optionally attaching any pending entries from stored_error_indications. Finally schedules on_ue_context_release_complete() which removes the ngap_ue_context.
UE Context Modification (Section 8.3.4). procedures/ngap_ue_context_modification_procedure.{h,cpp}. Coroutine. Validates UE-ID-pair consistency; persists new GUAMI, UE AMBR, and Core Network Assist Info for Inactive into the NGAP UE context; awaits CU-CP to apply bearer-level changes; responds with UEContextModificationResponse or Failure.
UE Radio Capability Info Indication (Section 8.14.1). Non-transaction RAN → AMF. ngap_impl::handle_tx_ue_radio_capability_info_indication_required() requests the radio capability RAT container from RRC via ue_notifier.on_ue_radio_access_cap_info_required() and wraps it in UERadioCapabilityInfoIndication. UE Radio Capability Check (Section 8.14.2) is not implemented; it is optional.
5. PDU Session Management (TS 38.413 Section 8.2)
These procedures are where NGAP reaches the deepest into the rest of the stack. NGAP itself only decodes/encodes the PDU and awaits CU-CP; CU-CP then fans out to F1AP, E1AP, and UPF.
sequenceDiagram
autonumber
participant AMF
participant NGAP as ngap_impl
participant CUCP as cu_cp
participant E1 as E1AP / CU-UP
participant F1 as F1AP / DU
participant RRC
AMF->>NGAP: PDUSessionResourceSetupRequest
NGAP->>CUCP: on_new_pdu_session_resource_setup_request()
par CU-CP orchestration
CUCP->>E1: BearerContextSetup (DRB · QoS)
E1-->>CUCP: BearerContextSetupResponse (CU-UP F1-U TEID)
CUCP->>F1: UEContextModification (DRB add · CU-UP TEID)
F1-->>CUCP: UEContextModificationResponse (DU F1-U TEID)
CUCP->>RRC: RRCReconfiguration (radioBearerConfig)
RRC-->>CUCP: RRCReconfigurationComplete
end
CUCP-->>NGAP: cu_cp_pdu_session_resource_setup_response
NGAP->>AMF: PDUSessionResourceSetupResponse (admit / fail list)PDU Session Resource Setup (Section 8.2.1). procedures/ngap_pdu_session_resource_setup_procedure.{h,cpp}. Coroutine. Stops the PDU-session request timer, validates the UE context (refuses if release_scheduled), extracts each setup item (session ID, QoS flow list, S-NSSAI, NAS PDU), stores UE AMBR if present, converts to cu_cp_pdu_session_resource_setup_request, and awaits CU-CP. The CU-CP side is the three-way dance shown above (E1AP → F1AP → RRC). Per-slice metrics are tallied under ngap_metrics_aggregator.
PDU Session Resource Modify (Section 8.2.2). procedures/ngap_pdu_session_resource_modify_procedure.{h,cpp}. Coroutine. Implements duplicate detection: the raw packed ASN.1 request is stored in ue_ctxt.last_pdu_session_resource_modify_request; a byte-exact duplicate returns an Error Indication rather than re-running the full procedure. On a fresh request, CU-CP runs E1AP Bearer Context Modification with QoS flow add/release/modify.
PDU Session Resource Release (Section 8.2.3). procedures/ngap_pdu_session_resource_release_procedure.{h,cpp}. Coroutine. Awaits CU-CP, which drives E1AP Bearer Context Release + F1AP UE Context Modification (DRB release) + GTP-U TEID teardown at CU-UP. Responds with the released-session list.
PDU Session Resource Notify (Section 8.2.5) and Modify Indication (Section 8.2.4 RAN-initiated) are not currently implemented; they are optional for a baseline gNB.
6. NAS Transport (TS 38.413 Section 8.6)
All three NAS transport paths are non-transaction messages, not procedures.
- DL NAS Transport (Section 8.6.2). procedures/ngap_dl_nas_message_transfer_procedure.{h,cpp}. Despite being a “transfer” and not a request-response, it is wrapped in a small coroutine because it may trigger an outbound
UERadioCapabilityInfoIndicationif the AMF sets the capability-request IE. Awaitsrrc_ue_notifier.on_new_pdu(nas_pdu)which delivers the NAS PDU down to the UE via SRB1/SRB2. - UL NAS Transport (Section 8.6.3).
ngap_impl::handle_ul_nas_transport_message(). Direct dispatch: wrap the NAS PDU plus user location info inULNASTransport, schedule TX on the per-UE executor. - NAS Non-Delivery Indication (Section 8.6.4). Not implemented.
7. Paging (TS 38.413 Section 8.5.1)
ngap_impl::handle_paging(paging_s&). Non-UE-associated message. Validated via validate_paging() in ngap_asn1_validators.h (must carry 5G-S-TMSI, at least one TAI, correct DRX IE), converted to cu_cp_paging_message, and forwarded to CU-CP’s paging handler. CU-CP fans out one F1AP Paging message per served cell that matches the TAI list; each DU then schedules the PDCCH P-RNTI transmission in its paging occasion.
8. Mobility (TS 38.413 Section 8.4)
NGAP participates only in the NG-based mobility paths. Xn-based handover lives in lib/xnap/ and never touches NGAP.
sequenceDiagram
autonumber
participant SRC as source gNB
participant AMF
participant TGT as target gNB
SRC->>AMF: HandoverRequired (target cell · src-to-tgt container)
AMF->>TGT: HandoverRequest (PDU sessions · security)
TGT->>AMF: HandoverRequestAck (DL TNL · tgt-to-src container)
AMF->>SRC: HandoverCommand (RRCReconfiguration inside)
Note over SRC: RRC sends HO cmd over Uu
Note over TGT: UE attaches · RACH · RRCReconfigComplete
TGT->>AMF: HandoverNotify (new user location)Handover Preparation (Section 8.4.1) — source side. procedures/ngap_handover_preparation_procedure.{h,cpp}. Coroutine returning async_task<ngap_handover_preparation_response>. Requests the source-to-target transparent container from RRC (carries measurements, K_gnb, security algorithms), builds HandoverRequired, subscribes to ue_ctxt.ev_mng.handover_preparation_outcome with tng_reloc_prep_ms timeout, sends, and awaits HandoverCommand or HandoverPreparationFailure. On success, extracts the target-to-source container (contains the RRC Handover Command to deliver over Uu). On timeout or failure, may trigger HandoverCancel.
Handover Resource Allocation (Section 8.4.2) — target side. procedures/ngap_handover_resource_allocation_procedure.{h,cpp}. Coroutine. Allocates a fresh ue_index via cu_cp_notifier.request_new_ue_index_allocation(), seeds the target-side security context, awaits CU-CP to run RRC setup and F1AP resource allocation on the target cell, then emits HandoverRequestAck with DL TNL info per PDU session — or HandoverFailure on rejection.
Handover Notify (Section 8.4.3). Non-transaction. Emitted by target gNB after RRCReconfigurationComplete from the UE, carrying the new user location info.
Path Switch Request (Section 8.4.4). procedures/ngap_path_switch_procedure.{h,cpp}. Used after an Xn-based handover or after an inactive-to-connected transition, to tell the AMF to update the UPF N3 path. Coroutine, UE-level transaction, ~5 s timeout.
Handover Cancel (Section 8.4.5). Integrated into the preparation procedure on timeout; no standalone class.
9. Warning / Trace / Location (Brief)
- PWS Write/Replace/Kill Warning (Section 8.8) — not implemented.
- Trace Start / Deactivate / Failure (Section 8.9) — not implemented.
- Location Reporting Control / Report / Failure Indication (Section 8.10) — partially implemented in
ngap_impl.cpp:handle_location_reporting_control_message(),handle_location_report_transmission(),handle_location_reporting_failure_indication_transmission(). Control message is forwarded to CU-CP’s location-reporting handler; the report/failure directions are built and dispatched on the UE executor.
10. AMF Connection Lifecycle (Brief)
ngap_connection_handler in lib/ngap/ngap_connection_handler.{h,cpp} wraps the N2 SCTP gateway for one AMF. Key members: amf_index, n2_connection_client& client_handler, ngap_message_handler& rx_pdu_handler (points back to ngap_impl), a connected_flag, and a manual_event_flag rx_path_disconnected used to notify coroutines of a link drop.
Bring-up: CU-CP calls connect_to_amf(); the gateway creates the SCTP association (SCTP PPID 60), the TX notifier becomes live, connected_flag is set, then handle_ng_setup_request() kicks off NG Setup. Teardown/loss: the gateway’s COMM_LOST event propagates into rx_path_disconnected; any in-flight coroutine awaiting a response times out. CU-CP retries the connect. Multi-AMF is just multiple ngap_impl instances inside ngap_repository; reselection and load balancing decisions (when a UE is first attached) are taken in CU-CP, not inside NGAP.
11. CU-CP ↔ NGAP Integration (Brief)
The contract is ngap_cu_cp_notifier (callable by NGAP) plus ngap_cu_cp_ue_notifier (per-UE handle returned by CU-CP). The CU-CP-side implementation lives in lib/cu_cp/adapters/ngap_adapters.h. Key callbacks:
on_new_ngap_ue(ue_index)— CU-CP creates acu_cp_ueand returns a UE notifier handle.on_new_initial_context_setup_request(...)— runs theinitial_context_setup_routinedescribed in Section 4.on_new_pdu_session_resource_setup_request(...)/..._modify_.../..._release_...— orchestrate E1AP + F1AP + RRC.on_new_ue_context_release_command(...)— releases RRC + F1AP + E1AP resources.on_paging_message(...)— fans out F1AP paging per cell.on_handover_request(...)/request_new_ue_index_allocation()/on_handover_request_received()— target-side handover setup.
Per-UE serialization: NGAP per-UE coroutines schedule themselves on the per-UE CU-CP task executor through ue_notifier.schedule_async_task(). This keeps the per-UE ordering invariant (same UE, same FIFO) across NGAP and CU-CP — no locks needed.
12. ASN.1 Layer (Brief)
The generated NGAP ASN.1 types live under asn1::ngap in lib/asn1/ngap/. The root PDU type is ngap_pdu_c, a CHOICE of init_msg_s, successful_outcome_s, unsuccessful_outcome_s. Every procedure has an ASN1_NGAP_ID_* integer code.
ngap_asn1_packer (in the CU-CP adapters) owns the SCTP edge. Inbound: handle_packed_pdu(byte_buffer) unpacks into ngap_pdu_c and dispatches to ngap_impl::handle_message(msg), which switches on PDU type and further on procedure code to call handle_initiating_message() / handle_successful_outcome() / handle_unsuccessful_outcome(). Outbound: the reverse — a logical message is packed and pushed to the SCTP gateway’s data handler.
Three helper files keep the procedure code clean: ngap_asn1_helpers.h (field-fill functions), ngap_asn1_converters.h (typed ↔ ASN.1 conversions including cause encoding), ngap_asn1_validators.h (IE presence/consistency checks before dispatch).
13. Procedure Framework
All NGAP procedures share the same shape: a small functor class with operator()(coro_context<async_task<R>>& ctx) that looks like
CORO_BEGIN(ctx);
transaction_sink.subscribe_to(event_source, timeout);
amf_notifier.on_new_message(request); // send
CORO_AWAIT(transaction_sink); // suspend until response/timeout
if (transaction_sink.successful()) { ... }
else if (transaction_sink.timeout_expired()) { ... }
else { ... } // explicit failure
CORO_RETURN(result);
The protocol_transaction_event_source<Success[, Failure]> is what the inbound PDU dispatcher sets when a matching SuccessfulOutcome / UnsuccessfulOutcome arrives. Global event sources sit in ngap_transaction_manager; per-UE event sources sit in ngap_ue_transaction_manager (HO Prep, HO Cancel, Path Switch, DL RAN Status Transfer).
Timers come in three scopes: context-level (procedure_timeout, request_pdu_session_timeout — seconds), procedure-level (NG Setup time_to_wait backoff; HO preparation tng_reloc_prep_timer), UE-level (the request_pdu_session_timer protecting against AMF silence after Initial UE Message).
14. Procedure Cross-Reference
| TS 38.413 § | Procedure | File | Class / Handler | Pattern |
|---|---|---|---|---|
| 8.7.1 | NG Setup | procedures/ng_setup_procedure.{h,cpp} | ng_setup_procedure | Coroutine, retry on time_to_wait |
| 8.7.3 | Error Indication | ngap_error_indication_helper.h + ngap_impl.cpp | send_error_indication() / handle_error_indication() | Inline out; log in |
| 8.7.4 | NG Reset | procedures/ng_reset_procedure.{h,cpp} | ng_reset_procedure | Coroutine |
| 8.6.1 | Initial UE Message | ngap_impl.cpp | handle_initial_ue_message() | Direct dispatch |
| 8.3.1 | Initial Context Setup | procedures/ngap_initial_context_setup_procedure.{h,cpp} | ngap_initial_context_setup_procedure | Coroutine, awaits CU-CP |
| 8.3.3 | UE Context Release Request | ngap_impl.cpp | handle_ue_context_release_request() | UE-executor async task |
| 8.3.2 | UE Context Release Cmd/Cpl | procedures/ngap_ue_context_release_procedure.{h,cpp} | ngap_ue_context_release_procedure | Coroutine |
| 8.3.4 | UE Context Modification | procedures/ngap_ue_context_modification_procedure.{h,cpp} | ngap_ue_context_modification_procedure | Coroutine |
| 8.14.1 | UE Radio Cap Info Ind | ngap_impl.cpp | handle_tx_ue_radio_capability_info_indication_required() | Direct + async tx |
| 8.2.1 | PDU Session Setup | procedures/ngap_pdu_session_resource_setup_procedure.{h,cpp} | ngap_pdu_session_resource_setup_procedure | Coroutine, awaits CU-CP fan-out |
| 8.2.2 | PDU Session Modify | procedures/ngap_pdu_session_resource_modify_procedure.{h,cpp} | ngap_pdu_session_resource_modify_procedure | Coroutine, duplicate-detect |
| 8.2.3 | PDU Session Release | procedures/ngap_pdu_session_resource_release_procedure.{h,cpp} | ngap_pdu_session_resource_release_procedure | Coroutine |
| 8.6.2 | DL NAS Transport | procedures/ngap_dl_nas_message_transfer_procedure.{h,cpp} | ngap_dl_nas_message_transfer_procedure | Coroutine (may trigger UE Radio Cap) |
| 8.6.3 | UL NAS Transport | ngap_impl.cpp | handle_ul_nas_transport_message() | UE-executor async task |
| 8.5.1 | Paging | ngap_impl.cpp | handle_paging() | Direct dispatch to paging_handler |
| 8.4.1 | HO Preparation | procedures/ngap_handover_preparation_procedure.{h,cpp} | ngap_handover_preparation_procedure | Coroutine, UE-level txn |
| 8.4.2 | HO Resource Allocation | procedures/ngap_handover_resource_allocation_procedure.{h,cpp} | ngap_handover_resource_allocation_procedure | Coroutine, new UE alloc |
| 8.4.3 | HO Notify | ngap_impl.cpp | handle_inter_cu_ho_rrc_recfg_complete() | Direct dispatch |
| 8.4.4 | Path Switch Request | procedures/ngap_path_switch_procedure.{h,cpp} | ngap_path_switch_procedure | Coroutine, UE-level txn |
| 8.10 | Location Reporting | ngap_impl.cpp | various handlers | Direct + async tx |
| 8.7.2 / 8.7.5 / 8.7.6 / 8.8 / 8.9 | AMF/RAN Config Update, Overload, PWS, Trace | — | — | Not implemented |
Entry points to read first in this order: lib/ngap/ngap_impl.h, lib/ngap/ngap_impl.cpp, lib/ngap/ue_context/, then any single file under lib/ngap/procedures/ — they all share the shape described in Section 13.