1use crate::llm::actions::{
6 client_trait::{Client, ClientActionResult},
7 protocol_trait::Protocol,
8 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
9};
10use crate::protocol::log_template::LogTemplate;
11use crate::protocol::{ConnectContext, EventType};
12use crate::server::ngap::{codec, nas};
13use crate::state::app_state::AppState;
14use anyhow::{anyhow, bail, Result};
15use serde_json::{json, Value};
16use std::sync::LazyLock;
17
18pub const INITIAL_UE: &str = "ngap_initial_ue_message";
19pub const UPLINK_NAS: &str = "ngap_uplink_nas_transport";
20pub const ERROR_INDICATION: &str = "ngap_error_indication";
21pub const NG_SETUP: &str = "ngap_ng_setup";
22
23pub const DEFAULT_MCC: &str = "001";
25pub const DEFAULT_MNC: &str = "01";
26pub const DEFAULT_GNB_ID: u64 = 1;
27pub const DEFAULT_GNB_ID_BITS: u64 = 32;
28pub const DEFAULT_TAC: u64 = 1;
29pub const DEFAULT_SST: u64 = 1;
30pub const DEFAULT_NR_CELL_ID: u64 = 0x10;
31pub const DEFAULT_RAN_NODE_NAME: &str = "netget-gnb";
32pub const DEFAULT_PAGING_DRX: &str = "v128";
33pub const DEFAULT_RRC_CAUSE: &str = "mo-Signalling";
34
35#[derive(Default)]
36pub struct NgapClientProtocol;
37impl NgapClientProtocol {
38 pub fn new() -> Self {
39 Self
40 }
41}
42
43fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
44 Parameter {
45 name: name.into(),
46 type_hint: type_hint.into(),
47 description: description.into(),
48 required,
49 }
50}
51
52fn action(
53 name: &str,
54 description: &str,
55 parameters: Vec<Parameter>,
56 example: Value,
57) -> ActionDefinition {
58 ActionDefinition {
59 name: name.into(),
60 description: description.into(),
61 parameters,
62 example,
63 log_template: Some(
64 LogTemplate::new()
65 .with_info(format!("-> NGAP {name}"))
66 .with_debug(format!("NGAP client {name}")),
67 ),
68 }
69}
70
71pub fn actions() -> Vec<ActionDefinition> {
72 vec![
73 action(
74 INITIAL_UE,
75 "Bring a UE onto N2: an Initial UE Message carrying a NAS message Rust builds from \
76 fields. A new RAN UE NGAP ID is allocated unless you give one. The UE's location is \
77 this gNB's cell and tracking area.",
78 vec![
79 p(
80 "nas",
81 "object",
82 "Usually {\"message\": \"registration_request\", \"registration_type\": \
83 \"initial\", \"mobile_identity\": {\"type\": \"suci\", \"plmn\": {\"mcc\": \
84 \"001\", \"mnc\": \"01\"}, \"routing_indicator\": \"0000\", \"msin\": \
85 \"0000000001\"}} (null-scheme SUCI), or a 5G-GUTI identity {\"type\": \
86 \"5g_guti\", \"plmn\", \"amf_region_id\", \"amf_set_id\", \"amf_pointer\", \
87 \"tmsi\"}",
88 true,
89 ),
90 p(
91 "ran_ue_ngap_id",
92 "number",
93 "Default: the next free one",
94 false,
95 ),
96 p(
97 "rrc_establishment_cause",
98 "string",
99 "mo-Signalling (default), mo-Data, emergency, mt-Access, …",
100 false,
101 ),
102 ],
103 json!({"type": INITIAL_UE, "nas": {"message": "registration_request",
104 "registration_type": "initial",
105 "mobile_identity": {"type": "suci", "plmn": {"mcc": "001", "mnc": "01"},
106 "routing_indicator": "0000", "msin": "0000000001"}}}),
107 ),
108 action(
109 UPLINK_NAS,
110 "Answer the AMF for a UE (Uplink NAS Transport). nas is structured: identity_response \
111 {mobile_identity}, authentication_failure {cause: mac_failure | synch_failure | \
112 non_5g_authentication_unacceptable}, registration_complete, \
113 deregistration_request_ue_originating {mobile_identity, switch_off}, \
114 security_mode_reject {cause}, 5gmm_status {cause}.",
115 vec![
116 p(
117 "amf_ue_ngap_id",
118 "number",
119 "From the AMF's ngap_downlink_nas_transport",
120 true,
121 ),
122 p("ran_ue_ngap_id", "number", "The UE's RAN UE NGAP ID", true),
123 p(
124 "nas",
125 "object",
126 "e.g. {\"message\": \"identity_response\", \"mobile_identity\": {...}}",
127 true,
128 ),
129 ],
130 json!({"type": UPLINK_NAS, "amf_ue_ngap_id": 1, "ran_ue_ngap_id": 1,
131 "nas": {"message": "authentication_failure", "cause": "mac_failure"}}),
132 ),
133 action(
134 ERROR_INDICATION,
135 "Report an error to the AMF (Error Indication), optionally for one UE.",
136 vec![
137 p(
138 "cause",
139 "string",
140 "group/value, e.g. protocol/semantic-error",
141 false,
142 ),
143 p(
144 "amf_ue_ngap_id",
145 "number",
146 "The UE concerned, if any",
147 false,
148 ),
149 p(
150 "ran_ue_ngap_id",
151 "number",
152 "The UE concerned, if any",
153 false,
154 ),
155 ],
156 json!({"type": ERROR_INDICATION, "cause": "protocol/semantic-error"}),
157 ),
158 action(
159 NG_SETUP,
160 "Repeat NG Setup with this gNB's configuration (it is done on connect already).",
161 vec![],
162 json!({"type": NG_SETUP}),
163 ),
164 action(
165 "disconnect",
166 "Close the association to the AMF.",
167 vec![],
168 json!({"type": "disconnect"}),
169 ),
170 ]
171}
172
173fn ev(id: &str, description: &str, params: Vec<Parameter>) -> EventType {
174 EventType::new(id, description, actions()[0].example.clone())
175 .with_parameters(params)
176 .with_actions(actions())
177}
178
179pub static CONNECTED_EVENT: LazyLock<EventType> =
180 LazyLock::new(|| {
181 ev(
182 "ngap_connected",
183 "NG Setup succeeded: the AMF admitted this gNB. Its name, GUAMIs and supported PLMNs and \
184 slices are given. Bring a UE in with ngap_initial_ue_message.",
185 vec![
186 p("amf_name", "string", "The name the AMF gave in its NG Setup Response", true),
187 p("served_guami", "array", "The AMF's GUAMIs", true),
188 p("plmn_support", "array", "PLMNs and slices it supports", true),
189 p("transport", "string", "sctp, or the non-standard lab tcp framing", true),
190 ],
191 )
192 });
193
194pub static DOWNLINK_NAS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
195 ev(
196 "ngap_downlink_nas_transport",
197 "The AMF sent a NAS message to a UE. nas is decoded: identity_request {identity_type}, \
198 authentication_request {ngksi, rand, autn}, registration_reject {cause}, …. Answer with \
199 ngap_uplink_nas_transport using the ids given.",
200 vec![
201 p("amf_ue_ngap_id", "number", "The AMF's id for the UE", true),
202 p("ran_ue_ngap_id", "number", "This gNB's id for the UE", true),
203 p("nas", "object", "The decoded 5GMM message", true),
204 ],
205 )
206});
207
208pub static RELEASE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
209 ev(
210 "ngap_ue_context_release",
211 "The AMF released a UE (UE Context Release Command). NetGet already answered with UE \
212 Context Release Complete; the UE is gone from this gNB.",
213 vec![
214 p("amf_ue_ngap_id", "number", "The released UE", true),
215 p(
216 "cause",
217 "object",
218 "The AMF's NGAP cause for the release, {\"group\", \"value\"}",
219 true,
220 ),
221 ],
222 )
223});
224
225pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
226 ev(
227 "ngap_amf_error",
228 "The AMF sent an Error Indication or NG Reset, or a PDU NetGet does not implement. \
229 Observational.",
230 vec![
231 p("message", "string", "What arrived: error_indication, ng_reset, or the name of a PDU NetGet does not implement", true),
232 p("cause", "object", "The NGAP cause the AMF gave, {\"group\", \"value\"}, when it gave one", false),
233 ],
234 )
235});
236
237pub fn plmn(ctx: &ConnectContext) -> Result<codec::Plmn> {
238 let get = |k: &str, d: &str| -> Result<String> {
239 Ok(ctx
240 .startup_params
241 .as_ref()
242 .map(|p| p.get_optional_string(k))
243 .transpose()?
244 .flatten()
245 .unwrap_or_else(|| d.to_string()))
246 };
247 Ok(codec::Plmn::from_json(
248 &json!({"mcc": get("mcc", DEFAULT_MCC)?, "mnc": get("mnc", DEFAULT_MNC)?}),
249 )?)
250}
251
252pub fn check(v: &Value) -> Result<()> {
254 match v["type"].as_str().unwrap_or_default() {
255 INITIAL_UE => {
256 nas::encode(v.get("nas").unwrap_or(&Value::Null), nas::UPLINK_MESSAGES)
257 .map_err(|e| anyhow!(e))?;
258 codec::json_opt_u64(v, "ran_ue_ngap_id", codec::MAX_RAN_UE_NGAP_ID)?;
259 }
260 UPLINK_NAS => {
261 codec::json_u64(v, "amf_ue_ngap_id", codec::MAX_AMF_UE_NGAP_ID)?;
262 codec::json_u64(v, "ran_ue_ngap_id", codec::MAX_RAN_UE_NGAP_ID)?;
263 nas::encode(v.get("nas").unwrap_or(&Value::Null), nas::UPLINK_MESSAGES)
264 .map_err(|e| anyhow!(e))?;
265 }
266 ERROR_INDICATION => {
267 if let Some(c) = v.get("cause").filter(|c| !c.is_null()) {
268 codec::Cause::from_json(c)?;
269 }
270 }
271 NG_SETUP => {}
272 other => bail!("Unknown NGAP client action {other:?}"),
273 }
274 Ok(())
275}
276
277fn param(
278 name: &str,
279 type_hint: &str,
280 description: &str,
281 example: Value,
282 default: Option<Value>,
283) -> ParameterDefinition {
284 ParameterDefinition {
285 name: name.into(),
286 type_hint: type_hint.into(),
287 description: description.into(),
288 required: false,
289 example,
290 default,
291 }
292}
293
294impl Protocol for NgapClientProtocol {
295 fn protocol_name(&self) -> &'static str {
296 "NGAP"
297 }
298 fn stack_name(&self) -> &'static str {
299 "ETH>IP>SCTP>NGAP"
300 }
301 fn keywords(&self) -> Vec<&'static str> {
302 vec!["ngap", "ngap client", "gnb", "n2", "5g ran"]
303 }
304 fn description(&self) -> &'static str {
305 "NGAP gNB (5G N2): sets up N2 with an AMF and brings UEs in with NAS registration"
306 }
307 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
308 actions()
309 }
310 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
311 vec![]
312 }
313 fn get_event_types(&self) -> Vec<EventType> {
314 vec![
315 CONNECTED_EVENT.clone(),
316 DOWNLINK_NAS_EVENT.clone(),
317 RELEASE_EVENT.clone(),
318 ERROR_EVENT.clone(),
319 ]
320 }
321 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
322 vec![
323 param("transport", "string", "\"sctp\" (default, TS 38.412) or \"tcp\" — NON-STANDARD length-prefixed framing for local work, which no real AMF speaks", json!("sctp"), None),
324 param("mcc", "string", "The gNB's PLMN: mobile country code, three digits", json!("001"), Some(json!(DEFAULT_MCC))),
325 param("mnc", "string", "The gNB's PLMN: mobile network code, two or three digits", json!("01"), Some(json!(DEFAULT_MNC))),
326 param("gnb_id", "number", "The gNB ID sent in NG Setup (its GlobalRANNodeID), gnb_id_bits wide", json!(1), Some(json!(DEFAULT_GNB_ID))),
327 param("gnb_id_bits", "number", "Length of the gNB ID in bits, 22-32", json!(32), Some(json!(DEFAULT_GNB_ID_BITS))),
328 param("tac", "number", "The tracking area code served (24 bits)", json!(1), Some(json!(DEFAULT_TAC))),
329 param("sst", "number", "Slice/service type broadcast (0-255)", json!(1), Some(json!(DEFAULT_SST))),
330 param("sd", "string", "Slice differentiator, six hex digits (default: none)", json!("010203"), None),
331 param("nr_cell_id", "number", "The NR cell identity UEs are reported in (36 bits)", json!(16), Some(json!(DEFAULT_NR_CELL_ID))),
332 param("ran_node_name", "string", "The RAN node name sent in NG Setup", json!("netget-gnb"), Some(json!(DEFAULT_RAN_NODE_NAME))),
333 ]
334 }
335 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
336 use crate::protocol::metadata::*;
337 ProtocolMetadataV2::builder()
338 .state(DevelopmentState::Experimental)
339 .privilege_requirement(PrivilegeRequirement::None)
340 .implementation("The AMF role's APER codec and 5GMM subset (src/server/ngap) as a gNB: SCTP association (PPID 60, stream 0/1) or the labelled lab TCP framing, NG Setup from the startup parameters on connect, RAN UE NGAP ID allocation, UE Context Release Complete and NG Reset Acknowledge answered in Rust")
341 .llm_control("Which UEs to bring in and with what NAS Registration Request (SUCI or 5G-GUTI, registration type), and how each UE answers the AMF's identity and authentication requests")
342 .e2e_testing("tests/client/ngap: Open5GS's AMF (open5gs-amfd) over real SCTP on Linux accepts NG Setup and processes the model's Registration Request; a canned AMF over the lab transport for bounds, follow-up depth and the model's actions")
343 .notes("Experimental: one independent AMF (Open5GS), runnable only where SCTP exists. No NAS security: RES* cannot be computed without a subscriber key, so registration ends at authentication. No PDU sessions or user plane. Handler chains stop after 8 follow-ups.")
344 .max_inbound_bytes(codec::MAX_PDU_LEN)
345 .build()
346 }
347 fn example_prompt(&self) -> &'static str {
348 "Connect a gNB for PLMN 001-01 to the AMF at 127.0.0.5 and register one UE with SUCI MSIN 0000000001"
349 }
350 fn get_startup_examples(&self) -> StartupExamples {
351 let llm = json!({"type": "open_client", "protocol": "ngap", "remote_addr": "127.0.0.5:38412",
352 "startup_params": {"mcc": "001", "mnc": "01", "tac": 1, "sst": 1},
353 "instruction": "Register one UE with a null-scheme SUCI (MSIN 0000000001) and answer the AMF's identity request with the same SUCI"});
354 let mut static_example = llm.clone();
355 static_example["event_handlers"] = json!([
356 {"event_pattern": "ngap_connected", "handler": {"type": "static", "actions": [actions()[0].example.clone()]}},
357 {"event_pattern": "*", "handler": {"type": "static", "actions": []}}
358 ]);
359 let mut scripted = llm.clone();
360 scripted["event_handlers"] = json!([{"event_pattern": "*", "handler": {"type": "script", "language": "python",
361 "code": "import json,sys\ni=json.load(sys.stdin); e=i['event']; t=i['event_type_id']\nsuci={'type':'suci','plmn':{'mcc':'001','mnc':'01'},'routing_indicator':'0000','msin':'0000000001'}\na=[]\nif t=='ngap_connected': a=[{'type':'ngap_initial_ue_message','nas':{'message':'registration_request','mobile_identity':suci}}]\nelif t=='ngap_downlink_nas_transport' and e['nas'].get('message')=='identity_request': a=[{'type':'ngap_uplink_nas_transport','amf_ue_ngap_id':e['amf_ue_ngap_id'],'ran_ue_ngap_id':e['ran_ue_ngap_id'],'nas':{'message':'identity_response','mobile_identity':suci}}]\nprint(json.dumps({'actions':a}))"}}]);
362 StartupExamples::new(llm, scripted, static_example)
363 }
364 fn group_name(&self) -> &'static str {
365 "Routing"
366 }
367}
368
369impl Client for NgapClientProtocol {
370 fn connect(
371 &self,
372 ctx: ConnectContext,
373 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
374 {
375 Box::pin(super::connect(ctx))
376 }
377 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
378 let name = v["type"].as_str().unwrap_or_default().to_string();
379 if name == "disconnect" {
380 return Ok(ClientActionResult::Disconnect);
381 }
382 check(&v)?;
383 Ok(ClientActionResult::Custom { name, data: v })
384 }
385}