1use crate::llm::actions::{
10 protocol_trait::{ActionResult, Protocol, Server},
11 ActionDefinition, Parameter,
12};
13use crate::protocol::log_template::LogTemplate;
14use crate::protocol::EventType;
15use crate::state::app_state::AppState;
16use anyhow::{anyhow, Context, Result};
17use serde_json::{json, Value};
18use std::sync::LazyLock;
19
20use super::codec::{self, Cause};
21use super::nas;
22
23pub struct NgapProtocol;
24
25impl Default for NgapProtocol {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl NgapProtocol {
32 pub fn new() -> Self {
33 Self
34 }
35}
36
37fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
38 Parameter {
39 name: name.to_string(),
40 type_hint: type_hint.to_string(),
41 description: description.to_string(),
42 required,
43 }
44}
45
46fn list<'a>(action: &'a Value, field: &str) -> Result<&'a Vec<Value>> {
47 action
48 .get(field)
49 .and_then(Value::as_array)
50 .with_context(|| format!("{field} must be a list"))
51}
52
53fn cause_field(action: &Value, field: &str, default: Option<&str>) -> Result<Cause> {
54 match action.get(field) {
55 Some(v) if !v.is_null() => Ok(Cause::from_json(v)?),
56 _ => match default {
57 Some(d) => Ok(Cause::from_json(&json!(d))?),
58 None => Err(anyhow!(
59 "{field} is required, e.g. \"misc/unknown-PLMN-or-SNPN\""
60 )),
61 },
62 }
63}
64
65pub fn downlink_nas(action: &Value) -> Result<Vec<u8>> {
67 let v = action.get("nas").context(
68 "nas is required: {\"message\": \"registration_reject\", \"cause\": \"illegal_ue\"}",
69 )?;
70 nas::encode(v, nas::DOWNLINK_MESSAGES).map_err(|e| anyhow!(e))
71}
72
73impl NgapProtocol {
74 fn ng_setup_response(&self, a: &Value) -> Result<ActionResult> {
75 let name = a
76 .get("amf_name")
77 .and_then(Value::as_str)
78 .context("amf_name is required")?;
79 let capacity = codec::json_opt_u64(a, "relative_amf_capacity", 255)?.unwrap_or(255);
80 Ok(ActionResult::Output(codec::ng_setup_response(
81 name,
82 list(a, "served_guami")?,
83 capacity,
84 list(a, "plmn_support")?,
85 )?))
86 }
87
88 fn ng_setup_failure(&self, a: &Value) -> Result<ActionResult> {
89 let cause = cause_field(a, "cause", None)?;
90 let ttw = a.get("time_to_wait").and_then(|v| match v {
91 Value::String(s) => Some(s.clone()),
92 Value::Number(n) => Some(n.to_string()),
93 _ => None,
94 });
95 Ok(ActionResult::Output(codec::ng_setup_failure(
96 &cause,
97 ttw.as_deref(),
98 )?))
99 }
100
101 fn downlink_nas_transport(&self, a: &Value) -> Result<ActionResult> {
102 let amf = codec::json_u64(a, "amf_ue_ngap_id", codec::MAX_AMF_UE_NGAP_ID)?;
103 let ran = codec::json_u64(a, "ran_ue_ngap_id", codec::MAX_RAN_UE_NGAP_ID)?;
104 let nas = downlink_nas(a)?;
105 Ok(ActionResult::Output(codec::downlink_nas_transport(
106 amf, ran, &nas,
107 )?))
108 }
109
110 fn ue_context_release(&self, a: &Value) -> Result<ActionResult> {
111 let amf = codec::json_u64(a, "amf_ue_ngap_id", codec::MAX_AMF_UE_NGAP_ID)?;
112 let ran = codec::json_opt_u64(a, "ran_ue_ngap_id", codec::MAX_RAN_UE_NGAP_ID)?;
113 let cause = cause_field(a, "cause", Some("nas/normal-release"))?;
114 Ok(ActionResult::Output(codec::ue_context_release_command(
115 amf, ran, &cause,
116 )?))
117 }
118
119 fn error_indication(&self, a: &Value) -> Result<ActionResult> {
120 let amf = codec::json_opt_u64(a, "amf_ue_ngap_id", codec::MAX_AMF_UE_NGAP_ID)?;
121 let ran = codec::json_opt_u64(a, "ran_ue_ngap_id", codec::MAX_RAN_UE_NGAP_ID)?;
122 let cause = match a.get("cause") {
123 Some(v) if !v.is_null() => Some(Cause::from_json(v)?),
124 _ => None,
125 };
126 Ok(ActionResult::Output(codec::error_indication(
127 amf,
128 ran,
129 cause.as_ref(),
130 )?))
131 }
132
133 fn ng_reset(&self, a: &Value) -> Result<ActionResult> {
134 let cause = cause_field(a, "cause", Some("misc/om-intervention"))?;
135 Ok(ActionResult::Output(codec::ng_reset(&cause, None)?))
136 }
137}
138
139const PLMN_HELP: &str = "{\"mcc\": \"001\", \"mnc\": \"01\"} — digits as strings";
144
145fn ng_setup_response_action() -> ActionDefinition {
146 ActionDefinition {
147 name: "send_ng_setup_response".into(),
148 description: "Admit this gNB: answer its NG Setup Request with an NG Setup Response \
149 naming this AMF, its GUAMIs and the PLMNs and slices it serves. This is the \
150 admission decision for N2 — return send_ng_setup_failure to refuse."
151 .into(),
152 parameters: vec![
153 p("amf_name", "string", "This AMF's name, 1-150 PrintableString characters", true),
154 p(
155 "served_guami",
156 "array",
157 &format!("GUAMIs this AMF serves (1-256): [{{\"plmn\": {PLMN_HELP}, \"amf_region_id\": 0-255, \"amf_set_id\": 0-1023, \"amf_pointer\": 0-63}}]"),
158 true,
159 ),
160 p(
161 "plmn_support",
162 "array",
163 "PLMNs and slices supported (1-12): [{\"plmn\": {...}, \"slices\": [{\"sst\": 1, \"sd\": \"010203\"}]}] — sd is optional, six hex digits",
164 true,
165 ),
166 p("relative_amf_capacity", "number", "0-255, default 255", false),
167 ],
168 example: json!({
169 "type": "send_ng_setup_response",
170 "amf_name": "netget-amf",
171 "served_guami": [{"plmn": {"mcc": "001", "mnc": "01"}, "amf_region_id": 2, "amf_set_id": 1, "amf_pointer": 0}],
172 "plmn_support": [{"plmn": {"mcc": "001", "mnc": "01"}, "slices": [{"sst": 1}]}],
173 "relative_amf_capacity": 255
174 }),
175 log_template: Some(
176 LogTemplate::new()
177 .with_info("-> NGAP NG Setup Response {amf_name}")
178 .with_debug("NGAP send_ng_setup_response {amf_name}"),
179 ),
180 }
181}
182
183fn ng_setup_failure_action() -> ActionDefinition {
184 ActionDefinition {
185 name: "send_ng_setup_failure".into(),
186 description: "Refuse this gNB with an NG Setup Failure carrying a cause. The explicit \
187 refusal of N2 setup."
188 .into(),
189 parameters: vec![
190 p(
191 "cause",
192 "string",
193 "group/value, e.g. misc/unknown-PLMN-or-SNPN, misc/control-processing-overload, \
194 misc/om-intervention, misc/unspecified, protocol/semantic-error",
195 true,
196 ),
197 p(
198 "time_to_wait",
199 "string",
200 "When the gNB may retry: v1s, v2s, v5s, v10s, v20s or v60s",
201 false,
202 ),
203 ],
204 example: json!({"type": "send_ng_setup_failure", "cause": "misc/unknown-PLMN-or-SNPN", "time_to_wait": "v10s"}),
205 log_template: Some(
206 LogTemplate::new()
207 .with_info("-> NGAP NG Setup Failure {cause}")
208 .with_debug("NGAP send_ng_setup_failure cause={cause}"),
209 ),
210 }
211}
212
213fn downlink_nas_action() -> ActionDefinition {
214 ActionDefinition {
215 name: "send_downlink_nas_transport".into(),
216 description:
217 "Send a NAS message to one UE in a Downlink NAS Transport. The NAS message is \
218 structured and Rust encodes it. Only what a UE accepts before NAS security \
219 exists is offered: registration_reject {cause}, identity_request \
220 {identity_type}, authentication_request {ngksi, rand?, autn? — 32 hex digits \
221 each, random when omitted; without the subscriber's key a real UE answers \
222 with authentication_failure mac_failure}, authentication_reject, \
223 service_reject {cause}, 5gmm_status {cause}."
224 .into(),
225 parameters: vec![
226 p(
227 "amf_ue_ngap_id",
228 "number",
229 "The UE's AMF UE NGAP ID, from the event",
230 true,
231 ),
232 p(
233 "ran_ue_ngap_id",
234 "number",
235 "The UE's RAN UE NGAP ID, from the event",
236 true,
237 ),
238 p(
239 "nas",
240 "object",
241 "e.g. {\"message\": \"registration_reject\", \"cause\": \"illegal_ue\"} or \
242 {\"message\": \"identity_request\", \"identity_type\": \"suci\"}. 5GMM causes by \
243 name: illegal_ue, plmn_not_allowed, tracking_area_not_allowed, congestion, \
244 no_network_slices_available, protocol_error_unspecified, …",
245 true,
246 ),
247 ],
248 example: json!({
249 "type": "send_downlink_nas_transport",
250 "amf_ue_ngap_id": 1,
251 "ran_ue_ngap_id": 1,
252 "nas": {"message": "registration_reject", "cause": "illegal_ue"}
253 }),
254 log_template: Some(
255 LogTemplate::new()
256 .with_info("-> NGAP Downlink NAS Transport ue={amf_ue_ngap_id}")
257 .with_debug("NGAP send_downlink_nas_transport ue={amf_ue_ngap_id} nas={nas}"),
258 ),
259 }
260}
261
262fn release_action() -> ActionDefinition {
263 ActionDefinition {
264 name: "send_ue_context_release_command".into(),
265 description: "Release one UE's N2 context (UE Context Release Command). The gNB answers \
266 with UE Context Release Complete, which NetGet handles."
267 .into(),
268 parameters: vec![
269 p("amf_ue_ngap_id", "number", "The UE's AMF UE NGAP ID", true),
270 p(
271 "ran_ue_ngap_id",
272 "number",
273 "The UE's RAN UE NGAP ID (sends the pair form)",
274 false,
275 ),
276 p(
277 "cause",
278 "string",
279 "group/value, default nas/normal-release",
280 false,
281 ),
282 ],
283 example: json!({"type": "send_ue_context_release_command", "amf_ue_ngap_id": 1, "ran_ue_ngap_id": 1, "cause": "nas/normal-release"}),
284 log_template: Some(
285 LogTemplate::new()
286 .with_info("-> NGAP UE Context Release Command ue={amf_ue_ngap_id}")
287 .with_debug("NGAP send_ue_context_release_command cause={cause}"),
288 ),
289 }
290}
291
292fn error_indication_action() -> ActionDefinition {
293 ActionDefinition {
294 name: "send_error_indication".into(),
295 description: "Report an error to the gNB with an Error Indication, optionally naming a UE."
296 .into(),
297 parameters: vec![
298 p(
299 "cause",
300 "string",
301 "group/value, e.g. protocol/semantic-error",
302 false,
303 ),
304 p(
305 "amf_ue_ngap_id",
306 "number",
307 "The UE concerned, if any",
308 false,
309 ),
310 p(
311 "ran_ue_ngap_id",
312 "number",
313 "The UE concerned, if any",
314 false,
315 ),
316 ],
317 example: json!({"type": "send_error_indication", "cause": "protocol/semantic-error"}),
318 log_template: Some(
319 LogTemplate::new()
320 .with_info("-> NGAP Error Indication {cause}")
321 .with_debug("NGAP send_error_indication cause={cause}"),
322 ),
323 }
324}
325
326fn ng_reset_action() -> ActionDefinition {
327 ActionDefinition {
328 name: "send_ng_reset".into(),
329 description: "Reset every UE context on this N2 interface (NG Reset, reset-all). The gNB \
330 acknowledges and NetGet clears its UE table."
331 .into(),
332 parameters: vec![p(
333 "cause",
334 "string",
335 "group/value, default misc/om-intervention",
336 false,
337 )],
338 example: json!({"type": "send_ng_reset", "cause": "misc/om-intervention"}),
339 log_template: Some(
340 LogTemplate::new()
341 .with_info("-> NGAP NG Reset {cause}")
342 .with_debug("NGAP send_ng_reset cause={cause}"),
343 ),
344 }
345}
346
347fn wait_for_more_action() -> ActionDefinition {
348 ActionDefinition {
349 name: "wait_for_more".into(),
350 description: "Send nothing. On an NG Setup Request or Initial UE Message this is NOT an \
351 admission: NetGet answers with a refusal."
352 .into(),
353 parameters: vec![],
354 example: json!({"type": "wait_for_more"}),
355 log_template: Some(
356 LogTemplate::new()
357 .with_info("-> NGAP wait for more")
358 .with_debug("NGAP wait_for_more"),
359 ),
360 }
361}
362
363fn response_actions() -> Vec<ActionDefinition> {
364 vec![
365 ng_setup_response_action(),
366 ng_setup_failure_action(),
367 downlink_nas_action(),
368 release_action(),
369 error_indication_action(),
370 ng_reset_action(),
371 wait_for_more_action(),
372 ]
373}
374
375pub static NGAP_NG_SETUP_EVENT: LazyLock<EventType> = LazyLock::new(|| {
380 EventType::new(
381 "ngap_ng_setup_request",
382 "A gNB sent NG Setup Request and wants to set up N2 with this AMF. global_ran_node_id \
383 names it (plmn, gnb_id), supported_ta_list lists its tracking areas with the PLMNs and \
384 slices broadcast in each. Reply with send_ng_setup_response to admit it, or \
385 send_ng_setup_failure to refuse. Anything else — wait_for_more, no answer, a failed \
386 model — is answered by NetGet with NG Setup Failure: admitting a gNB is never the \
387 default.",
388 ng_setup_response_action().example,
389 )
390 .with_alternative_example(
391 json!({"type": "send_ng_setup_failure", "cause": "misc/unknown-PLMN-or-SNPN"}),
392 )
393 .with_actions(response_actions())
394 .with_log_template(
395 LogTemplate::new()
396 .with_info("NGAP NG Setup Request from {ran_node_name}")
397 .with_debug("NGAP NG Setup Request {global_ran_node_id}")
398 .with_trace("NGAP NG Setup Request: {json_pretty(.)}"),
399 )
400});
401
402pub static NGAP_INITIAL_UE_MESSAGE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
403 EventType::new(
404 "ngap_initial_ue_message",
405 "A UE's first NAS message arrived through the gNB (Initial UE Message). NetGet allocated \
406 amf_ue_ngap_id; ran_ue_ngap_id is the gNB's. nas is the decoded 5GMM message — for a \
407 registration_request: registration_type, ngksi and mobile_identity (a SUCI's plmn, \
408 routing_indicator, protection_scheme and, for the null scheme, msin; or a 5G-GUTI). \
409 Answer with send_downlink_nas_transport carrying identity_request, \
410 authentication_request or registration_reject, optionally followed by \
411 send_ue_context_release_command. Saying nothing makes NetGet reject the UE \
412 (registration_reject protocol_error_unspecified) and release it.",
413 downlink_nas_action().example,
414 )
415 .with_alternative_example(json!({
416 "type": "send_downlink_nas_transport",
417 "amf_ue_ngap_id": 1,
418 "ran_ue_ngap_id": 1,
419 "nas": {"message": "authentication_request", "ngksi": 0}
420 }))
421 .with_actions(response_actions())
422 .with_log_template(
423 LogTemplate::new()
424 .with_info("NGAP Initial UE Message ue={amf_ue_ngap_id}")
425 .with_debug("NGAP Initial UE Message ue={amf_ue_ngap_id} nas={nas}")
426 .with_trace("NGAP Initial UE Message: {json_pretty(.)}"),
427 )
428});
429
430pub static NGAP_UPLINK_NAS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
431 EventType::new(
432 "ngap_uplink_nas_transport",
433 "A UE answered through the gNB (Uplink NAS Transport). nas is the decoded 5GMM message — \
434 e.g. identity_response with its mobile_identity, or authentication_failure with its \
435 cause (mac_failure means the UE did not accept the AUTN). Answer with \
436 send_downlink_nas_transport, send_ue_context_release_command, or wait_for_more.",
437 json!({
438 "type": "send_downlink_nas_transport",
439 "amf_ue_ngap_id": 1,
440 "ran_ue_ngap_id": 1,
441 "nas": {"message": "registration_reject", "cause": "illegal_ue"}
442 }),
443 )
444 .with_alternative_example(json!({"type": "wait_for_more"}))
445 .with_actions(response_actions())
446 .with_log_template(
447 LogTemplate::new()
448 .with_info("NGAP Uplink NAS Transport ue={amf_ue_ngap_id}")
449 .with_debug("NGAP Uplink NAS Transport ue={amf_ue_ngap_id} nas={nas}")
450 .with_trace("NGAP Uplink NAS Transport: {json_pretty(.)}"),
451 )
452});
453
454pub static NGAP_ERROR_INDICATION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
455 EventType::new(
456 "ngap_error_indication",
457 "The gNB reported an error (Error Indication), with an optional cause and UE ids. \
458 Observational: no answer is required. Release the UE with \
459 send_ue_context_release_command if it names one, or wait_for_more.",
460 json!({"type": "wait_for_more"}),
461 )
462 .with_alternative_example(
463 json!({"type": "send_ue_context_release_command", "amf_ue_ngap_id": 1}),
464 )
465 .with_actions(response_actions())
466 .with_log_template(
467 LogTemplate::new()
468 .with_info("NGAP Error Indication {cause}")
469 .with_debug("NGAP Error Indication {cause} ue={amf_ue_ngap_id}")
470 .with_trace("NGAP Error Indication: {json_pretty(.)}"),
471 )
472});
473
474impl Protocol for NgapProtocol {
477 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
480 vec![]
481 }
482
483 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
484 response_actions()
485 }
486
487 fn protocol_name(&self) -> &'static str {
488 "NGAP"
489 }
490
491 fn get_event_types(&self) -> Vec<EventType> {
492 vec![
493 NGAP_NG_SETUP_EVENT.clone(),
494 NGAP_INITIAL_UE_MESSAGE_EVENT.clone(),
495 NGAP_UPLINK_NAS_EVENT.clone(),
496 NGAP_ERROR_INDICATION_EVENT.clone(),
497 ]
498 }
499
500 fn stack_name(&self) -> &'static str {
501 "ETH>IP>SCTP>NGAP"
502 }
503
504 fn keywords(&self) -> Vec<&'static str> {
505 vec!["ngap", "n2", "amf", "5g core", "5gc", "38.413"]
506 }
507
508 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
509 use crate::protocol::metadata::{
510 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
511 };
512 ProtocolMetadataV2::builder()
513 .state(DevelopmentState::Experimental)
514 .privilege_requirement(PrivilegeRequirement::None)
516 .well_known_sctp_port(38412)
519 .implementation(
520 "TS 38.413 ALIGNED PER hand-written for the procedures implemented (NG Setup, \
521 Initial UE Message, Downlink/Uplink NAS Transport, Error Indication, NG Reset, \
522 UE Context Release), other IEs framed and skipped by length; plain 5GMM (TS \
523 24.501) decoded and the pre-security downlink messages encoded; SCTP via a \
524 socket2 one-to-one socket with PPID 60 and stream 0/1 selection, plus a \
525 non-standard length-prefixed TCP transport for lab use",
526 )
527 .llm_control(
528 "The model admits or refuses gNBs (NG Setup Response/Failure with a cause) and \
529 decides what each UE is told (Identity Request, Authentication Request, \
530 Registration Reject with a 5GMM cause, UE Context Release). Decoding, ID \
531 allocation, NG Reset and every protocol-validity refusal are Rust's",
532 )
533 .e2e_testing(
534 "UERANSIM's nr-gnb and nr-ue (asn1c APER) set up N2 and register against it over \
535 real SCTP on Linux, with tshark's ngap dissector reading a live capture; over \
536 the lab TCP transport with a mocked model on any host, including the \
537 fail-closed paths, the decode bounds and the pcap oracle on NetGet's own PDUs",
538 )
539 .notes(
540 "EXPERIMENTAL: one independent peer (UERANSIM), and its evidence runs only where \
541 SCTP exists (Linux, the ngap pair job). macOS has no SCTP stack, so there the \
542 default transport refuses to start by name and only the non-standard \
543 transport=\"tcp\" framing runs. NAS security is not implemented: no 5G-AKA \
544 derivation, no Security Mode Command, no integrity or ciphering — a real UE \
545 answers NetGet's Authentication Request with MAC failure, and Registration \
546 Accept cannot be sent. No PDU sessions, handover, paging or AMF \
547 configuration update.",
548 )
549 .max_inbound_bytes(codec::MAX_PDU_LEN)
550 .build()
551 }
552
553 fn description(&self) -> &'static str {
554 "NGAP (5G N2) AMF: NG Setup and initial UE registration signalling with gNBs"
555 }
556
557 fn example_prompt(&self) -> &'static str {
558 "Start a 5G AMF for PLMN 001-01 that admits gNBs and rejects every UE as illegal"
559 }
560
561 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
562 vec![crate::llm::actions::ParameterDefinition {
563 name: "transport".into(),
564 type_hint: "string".into(),
565 description: "\"sctp\" (default — TS 38.412, PPID 60) or \"tcp\". TCP IS \
566 NON-STANDARD: a NetGet length-prefixed framing that no real gNB speaks, \
567 for local protocol work on hosts with no SCTP (macOS has none). A host \
568 without SCTP refuses \"sctp\" rather than falling back."
569 .into(),
570 required: false,
571 example: json!("sctp"),
572 default: None,
573 }]
574 }
575
576 fn group_name(&self) -> &'static str {
577 "Routing"
578 }
579
580 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
581 let script = r#"import json, sys
582data = json.load(sys.stdin)
583event = data["event"]
584if data["event_type_id"] == "ngap_ng_setup_request":
585 plmn = event["global_ran_node_id"]["plmn"]
586 actions = [{"type": "send_ng_setup_response", "amf_name": "netget-amf",
587 "served_guami": [{"plmn": plmn, "amf_region_id": 2, "amf_set_id": 1, "amf_pointer": 0}],
588 "plmn_support": [{"plmn": plmn, "slices": [{"sst": 1}]}]}]
589elif data["event_type_id"] == "ngap_initial_ue_message":
590 actions = [{"type": "send_downlink_nas_transport", "amf_ue_ngap_id": event["amf_ue_ngap_id"],
591 "ran_ue_ngap_id": event["ran_ue_ngap_id"],
592 "nas": {"message": "registration_reject", "cause": "illegal_ue"}}]
593else:
594 actions = []
595print(json.dumps({"actions": actions}))"#;
596 crate::llm::actions::StartupExamples::new(
597 json!({
598 "type": "open_server",
599 "port": 38412,
600 "base_stack": "ngap",
601 "instruction": "Act as a 5G AMF for PLMN 001-01, slice SST 1. Admit gNBs that \
602 broadcast that PLMN; ask every registering UE for its identity, \
603 then reject it with illegal_ue.",
604 "startup_params": {"transport": "sctp"}
605 }),
606 json!({
607 "type": "open_server",
608 "port": 38412,
609 "base_stack": "ngap",
610 "startup_params": {"transport": "sctp"},
611 "event_handlers": [{
612 "event_pattern": "*",
613 "handler": {"type": "script", "language": "python", "code": script}
614 }]
615 }),
616 json!({
617 "type": "open_server",
618 "port": 38412,
619 "base_stack": "ngap",
620 "startup_params": {"transport": "sctp"},
621 "event_handlers": [{
622 "event_pattern": "ngap_ng_setup_request",
623 "handler": {"type": "static", "actions": [{
624 "type": "send_ng_setup_failure",
625 "cause": "misc/om-intervention",
626 "time_to_wait": "v60s"
627 }]}
628 }]
629 }),
630 )
631 }
632}
633
634impl Server for NgapProtocol {
635 fn spawn(
636 &self,
637 ctx: crate::protocol::SpawnContext,
638 ) -> std::pin::Pin<
639 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
640 > {
641 Box::pin(async move {
642 super::NgapServer::spawn_with_llm_actions(
643 ctx.legacy_listen_addr(),
644 ctx.llm_client,
645 ctx.state,
646 ctx.status_tx,
647 ctx.server_id,
648 ctx.startup_params,
649 )
650 .await
651 })
652 }
653
654 fn execute_action(&self, action: Value) -> Result<ActionResult> {
655 let t = action
656 .get("type")
657 .and_then(Value::as_str)
658 .context("Missing action type")?;
659 match t {
660 "send_ng_setup_response" => self.ng_setup_response(&action),
661 "send_ng_setup_failure" => self.ng_setup_failure(&action),
662 "send_downlink_nas_transport" => self.downlink_nas_transport(&action),
663 "send_ue_context_release_command" => self.ue_context_release(&action),
664 "send_error_indication" => self.error_indication(&action),
665 "send_ng_reset" => self.ng_reset(&action),
666 "wait_for_more" => Ok(ActionResult::WaitForMore),
667 "close_connection" => Ok(ActionResult::CloseConnection),
669 _ => Err(anyhow!("Unknown NGAP action type: {t}")),
670 }
671 }
672}