1pub mod actions;
38pub mod aper;
39pub mod codec;
40pub mod nas;
41pub mod transport;
42
43use anyhow::Result;
44use serde_json::{json, Value};
45use std::collections::BTreeMap;
46use std::net::SocketAddr;
47use std::sync::Arc;
48use tokio::sync::{mpsc, Mutex};
49use tracing::{debug, error, info, warn};
50
51use crate::llm::action_helper::call_llm;
52use crate::llm::actions::protocol_trait::ActionResult;
53use crate::llm::ollama_client::OllamaClient;
54use crate::logging::emit::Log;
55use crate::protocol::Event;
56use crate::server::connection::ConnectionId;
57use crate::state::app_state::AppState;
58use crate::state::ServerId;
59use crate::utils::WireFailure;
60
61use actions::{
62 NgapProtocol, NGAP_ERROR_INDICATION_EVENT, NGAP_INITIAL_UE_MESSAGE_EVENT, NGAP_NG_SETUP_EVENT,
63 NGAP_UPLINK_NAS_EVENT,
64};
65use codec::{Cause, PduKind};
66use transport::{MessageWriter, ReadOutcome, Transport};
67
68const FIRST_MESSAGE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
71
72const IDLE_BETWEEN_MESSAGES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3600);
77
78const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
80
81const CONNECTION_CAP_REFUSAL: &[u8] = &[];
86
87pub const MAX_UE_CONTEXTS: usize = 4096;
90
91pub struct NgapServer;
93
94impl NgapServer {
95 pub async fn spawn_with_llm_actions(
97 listen_addr: SocketAddr,
98 llm_client: OllamaClient,
99 app_state: Arc<AppState>,
100 status_tx: mpsc::UnboundedSender<String>,
101 server_id: ServerId,
102 startup_params: Option<crate::protocol::StartupParams>,
103 ) -> Result<SocketAddr> {
104 let transport = Transport::from_params(startup_params.as_ref())?;
105 let listener = transport::bind_listener(listen_addr, transport).await?;
106 let local_addr = listener.local_addr()?;
107 Log::new(Some(&status_tx)).info(format!(
108 "NGAP AMF listening on {} over {}",
109 local_addr,
110 transport.label()
111 ));
112 if transport == Transport::Tcp {
113 Log::new(Some(&status_tx)).warn(
114 "NGAP is running over TCP with a NetGet length prefix. This is NOT N2: TS 38.412 \
115 defines NGAP over SCTP only, and no real gNB will connect. Use it for local \
116 protocol work and nothing else.",
117 );
118 }
119
120 let protocol = Arc::new(NgapProtocol::new());
121 let task_registrar = app_state.clone();
122 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
123 let accept_handle = tokio::spawn(async move {
124 loop {
125 match crate::server::accept_bounded::accept_bounded(
126 &listener,
127 &limiter,
128 CONNECTION_CAP_REFUSAL,
129 "NGAP",
130 Some(&status_tx),
131 )
132 .await
133 {
134 Ok((stream, remote_addr, permit)) => {
135 let connection_id =
136 ConnectionId::new(app_state.get_next_unified_id().await);
137 Log::new(Some(&status_tx)).info(format!(
138 "NGAP association {} from {} ({})",
139 connection_id,
140 remote_addr,
141 transport.as_str()
142 ));
143 register_connection(
144 &app_state,
145 server_id,
146 connection_id,
147 remote_addr,
148 transport,
149 )
150 .await;
151 let _ = status_tx.send("__UPDATE_UI__".to_string());
152 let conn = transport::Conn::new(stream, transport);
153 let llm = llm_client.clone();
154 let state = app_state.clone();
155 let status = status_tx.clone();
156 let protocol = protocol.clone();
157 let owner = app_state.clone();
158 owner
159 .spawn_server_task(server_id, async move {
160 let _permit = permit;
161 if let Err(e) = run_session(
162 conn,
163 connection_id,
164 server_id,
165 remote_addr,
166 llm,
167 state.clone(),
168 status.clone(),
169 protocol,
170 transport,
171 )
172 .await
173 {
174 Log::new(Some(&status))
175 .error(format!("NGAP association error: {e}"));
176 }
177 state
178 .remove_peer_handle(server_id, connection_id.as_u32())
179 .await;
180 state
181 .close_connection_on_server(server_id, connection_id)
182 .await;
183 Log::new(Some(&status))
184 .info(format!("NGAP association {connection_id} closed"));
185 let _ = status.send("__UPDATE_UI__".to_string());
186 })
187 .await;
188 }
189 Err(e) => {
190 Log::new(Some(&status_tx))
191 .error(format!("Failed to accept NGAP association: {e}"));
192 break;
193 }
194 }
195 }
196 });
197 task_registrar
198 .register_server_task(server_id, accept_handle)
199 .await;
200 Ok(local_addr)
201 }
202}
203
204async fn register_connection(
205 app_state: &AppState,
206 server_id: ServerId,
207 connection_id: ConnectionId,
208 remote_addr: SocketAddr,
209 transport: Transport,
210) {
211 use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
212 let now = crate::utils::clock::Instant::now();
213 app_state
214 .add_connection_to_server(
215 server_id,
216 ConnectionState {
217 id: connection_id,
218 remote_addr,
219 local_addr: remote_addr,
220 bytes_sent: 0,
221 bytes_received: 0,
222 packets_sent: 0,
223 packets_received: 0,
224 last_activity: now,
225 status: ConnectionStatus::Active,
226 status_changed_at: now,
227 protocol_info: ProtocolConnectionInfo::new(json!({
228 "state": "awaiting NG Setup",
229 "transport": transport.label(),
230 })),
231 },
232 )
233 .await;
234}
235
236struct Ue {
238 ran_ue_ngap_id: u64,
239 initial_nas: Option<u8>,
242}
243
244struct Session {
245 connection_id: ConnectionId,
246 server_id: ServerId,
247 remote_addr: SocketAddr,
248 llm_client: OllamaClient,
249 app_state: Arc<AppState>,
250 status_tx: mpsc::UnboundedSender<String>,
251 protocol: Arc<NgapProtocol>,
252 transport: Transport,
253 writer: Arc<Mutex<MessageWriter>>,
254 gnb: Option<Value>,
256 ues: BTreeMap<u64, Ue>,
257 next_amf_ue_ngap_id: u64,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
262struct Sent {
263 kind: PduKind,
264 code: u8,
265 amf_ue_ngap_id: Option<u64>,
266}
267
268#[derive(Debug, Clone)]
269enum HandlerOutcome {
270 NoPolicy,
271 Failed(WireFailure),
272 Ran { sent: Vec<Sent>, closed: bool },
273}
274
275#[allow(clippy::too_many_arguments)]
276async fn run_session(
277 conn: transport::Conn,
278 connection_id: ConnectionId,
279 server_id: ServerId,
280 remote_addr: SocketAddr,
281 llm_client: OllamaClient,
282 app_state: Arc<AppState>,
283 status_tx: mpsc::UnboundedSender<String>,
284 protocol: Arc<NgapProtocol>,
285 transport: Transport,
286) -> Result<()> {
287 let mut reader = conn.reader();
288 let writer = Arc::new(Mutex::new(conn.writer()));
289 let peer_rx = crate::server::peer_support::register_peer_channel(
290 &app_state,
291 server_id,
292 connection_id.as_u32(),
293 )
294 .await;
295 crate::server::peer_support::spawn_peer_command_task(
296 peer_rx,
297 protocol.clone(),
298 app_state.clone(),
299 server_id,
300 connection_id.as_u32(),
301 writer.clone(),
302 status_tx.clone(),
303 );
304 let mut session = Session {
305 connection_id,
306 server_id,
307 remote_addr,
308 llm_client,
309 app_state,
310 status_tx,
311 protocol,
312 transport,
313 writer,
314 gnb: None,
315 ues: BTreeMap::new(),
316 next_amf_ue_ngap_id: 1,
317 };
318
319 let mut received_one = false;
320 loop {
321 let deadline = if received_one {
324 IDLE_BETWEEN_MESSAGES_TIMEOUT
325 } else {
326 FIRST_MESSAGE_READ_TIMEOUT
327 };
328 match reader.next(deadline).await {
329 ReadOutcome::Message(bytes) => {
330 received_one = true;
331 session
332 .app_state
333 .update_connection_stats(
334 server_id,
335 connection_id,
336 Some(bytes.len() as u64),
337 None,
338 Some(1),
339 None,
340 )
341 .await;
342 if !session.dispatch(&bytes).await {
343 break;
344 }
345 }
346 ReadOutcome::TooLarge(n) => {
347 received_one = true;
348 Log::new(Some(&session.status_tx)).warn(format!(
349 "NGAP message of {n} octets from {} exceeds {} (decision=refused_oversize)",
350 remote_addr,
351 codec::MAX_PDU_LEN
352 ));
353 session
354 .send_error_indication(
355 None,
356 None,
357 Cause::protocol(codec::PROTOCOL_TRANSFER_SYNTAX_ERROR),
358 )
359 .await;
360 if transport == Transport::Tcp {
361 break;
363 }
364 }
365 ReadOutcome::TimedOut => {
366 debug!("NGAP association {connection_id} idle past its deadline; closing");
367 break;
368 }
369 ReadOutcome::Eof => break,
370 ReadOutcome::Error(e) => {
371 debug!("NGAP association {connection_id} read error: {e}");
372 break;
373 }
374 }
375 }
376 session.writer.lock().await.close();
377 Ok(())
378}
379
380impl Session {
381 async fn dispatch(&mut self, bytes: &[u8]) -> bool {
383 let pdu = match codec::decode_pdu(bytes) {
384 Ok(p) => p,
385 Err(e) => {
386 Log::new(Some(&self.status_tx)).warn(format!(
388 "NGAP PDU from {} refused: {e} (decision=refused_malformed)",
389 self.remote_addr
390 ));
391 self.send_error_indication(None, None, e.cause).await;
392 return true;
393 }
394 };
395 debug!("NGAP {} from {}", pdu.label(), self.remote_addr);
396 match (pdu.kind, pdu.procedure_code) {
397 (PduKind::Initiating, codec::PROC_NG_SETUP) => self.on_ng_setup(&pdu).await,
398 (PduKind::Initiating, codec::PROC_ERROR_INDICATION) => {
399 self.on_error_indication(&pdu).await
400 }
401 (PduKind::Initiating, _) if self.gnb.is_none() => {
402 Log::new(Some(&self.status_tx)).warn(format!(
403 "NGAP {} from {} before NG Setup completed; refusing",
404 pdu.label(),
405 self.remote_addr
406 ));
407 self.send_error_indication(
408 None,
409 None,
410 Cause::protocol(codec::PROTOCOL_MESSAGE_NOT_COMPATIBLE_WITH_RECEIVER_STATE),
411 )
412 .await;
413 true
414 }
415 (PduKind::Initiating, codec::PROC_INITIAL_UE_MESSAGE) => self.on_initial_ue(&pdu).await,
416 (PduKind::Initiating, codec::PROC_UPLINK_NAS_TRANSPORT) => {
417 self.on_uplink_nas(&pdu).await
418 }
419 (PduKind::Initiating, codec::PROC_NG_RESET) => self.on_ng_reset(&pdu).await,
420 (PduKind::Successful, codec::PROC_UE_CONTEXT_RELEASE) => {
421 self.on_release_complete(&pdu).await
422 }
423 (PduKind::Successful, codec::PROC_NG_RESET) => {
424 let n = self.ues.len();
425 self.ues.clear();
426 self.set_connection_state().await;
427 info!(
428 "NGAP NG Reset acknowledged by {}; {n} UE contexts cleared",
429 self.remote_addr
430 );
431 true
432 }
433 (PduKind::Initiating, _) => {
434 let cause = match pdu.criticality {
437 codec::Criticality::Reject => {
438 Some(codec::PROTOCOL_ABSTRACT_SYNTAX_ERROR_REJECT)
439 }
440 codec::Criticality::Notify => Some(2),
441 codec::Criticality::Ignore => None,
442 };
443 Log::new(Some(&self.status_tx)).warn(format!(
444 "NGAP {} from {} is not implemented by NetGet's AMF",
445 pdu.label(),
446 self.remote_addr
447 ));
448 if let Some(c) = cause {
449 self.send_error_indication(None, None, Cause::protocol(c))
450 .await;
451 }
452 true
453 }
454 _ => {
455 debug!(
456 "NGAP {} from {} answers nothing NetGet asked; ignored",
457 pdu.label(),
458 self.remote_addr
459 );
460 true
461 }
462 }
463 }
464
465 async fn on_ng_setup(&mut self, pdu: &codec::Pdu) -> bool {
468 let info = match codec::parse_ng_setup_request(pdu) {
469 Ok(v) => v,
470 Err(e) => {
471 Log::new(Some(&self.status_tx)).warn(format!(
472 "NGAP NG Setup Request from {} refused before the model: {e}",
473 self.remote_addr
474 ));
475 self.send_encoded("NG Setup Failure", codec::ng_setup_failure(&e.cause, None))
476 .await;
477 return true;
478 }
479 };
480 let mut data = info.clone();
481 data["transport"] = json!(self.transport.label());
482 let event = Event::new(&NGAP_NG_SETUP_EVENT, data);
483 let event_id = event.event_type.id.clone();
484 match self.call_llm_and_send(&event).await {
485 HandlerOutcome::Ran { ref sent, closed }
486 if sent
487 .iter()
488 .any(|s| s.kind == PduKind::Successful && s.code == codec::PROC_NG_SETUP) =>
489 {
490 self.gnb = Some(json!({
491 "global_ran_node_id": info["global_ran_node_id"],
492 "ran_node_name": info["ran_node_name"],
493 }));
494 self.set_connection_state().await;
495 Log::new(Some(&self.status_tx)).info(format!(
496 "NGAP NG Setup accepted for {} ({})",
497 self.remote_addr,
498 info["ran_node_name"].as_str().unwrap_or("unnamed gNB")
499 ));
500 !closed
501 }
502 HandlerOutcome::Ran { ref sent, closed }
503 if sent
504 .iter()
505 .any(|s| s.kind == PduKind::Unsuccessful && s.code == codec::PROC_NG_SETUP) =>
506 {
507 info!(
508 "NGAP {event_id} decision=model_reject peer={}",
509 self.remote_addr
510 );
511 !closed
512 }
513 HandlerOutcome::Ran { closed, .. } => {
514 Log::new(Some(&self.status_tx)).warn(format!(
515 "NGAP {event_id} produced no NG Setup answer for {} (decision=model_silent); \
516 refusing with misc/unspecified",
517 self.remote_addr
518 ));
519 self.send_encoded(
520 "NG Setup Failure",
521 codec::ng_setup_failure(&Cause::misc(codec::MISC_UNSPECIFIED), None),
522 )
523 .await;
524 !closed
525 }
526 HandlerOutcome::Failed(failure) => {
527 let (cause, wait) = if failure.is_overloaded() {
528 (Cause::misc(codec::MISC_CONTROL_PROCESSING_OVERLOAD), "v5s")
529 } else {
530 (Cause::misc(codec::MISC_UNSPECIFIED), "v10s")
531 };
532 Log::new(Some(&self.status_tx)).warn(format!(
533 "NGAP {event_id} could not be evaluated for {} \
534 (decision=fail_closed_llm_error, {}); refusing with {}",
535 self.remote_addr,
536 failure_category(failure),
537 cause.label()
538 ));
539 self.send_encoded(
540 "NG Setup Failure",
541 codec::ng_setup_failure(&cause, Some(wait)),
542 )
543 .await;
544 true
545 }
546 HandlerOutcome::NoPolicy => {
547 Log::new(Some(&self.status_tx)).warn(format!(
548 "NGAP {event_id} refused for {} (decision=no_policy_configured): this AMF has \
549 no instruction and no event handler, so nothing decides which gNBs may set \
550 up N2. Give the server an instruction, or a handler for {event_id}.",
551 self.remote_addr
552 ));
553 self.send_encoded(
554 "NG Setup Failure",
555 codec::ng_setup_failure(&Cause::misc(codec::MISC_OM_INTERVENTION), None),
556 )
557 .await;
558 true
559 }
560 }
561 }
562
563 async fn on_initial_ue(&mut self, pdu: &codec::Pdu) -> bool {
564 let ue = match codec::parse_initial_ue_message(pdu) {
565 Ok(u) => u,
566 Err(e) => {
567 let ran = pdu
568 .ie(codec::IE_RAN_UE_NGAP_ID)
569 .and_then(|ie| codec::decode_ran_ue_ngap_id(ie).ok());
570 Log::new(Some(&self.status_tx)).warn(format!(
571 "NGAP Initial UE Message from {} refused before the model: {e}",
572 self.remote_addr
573 ));
574 self.send_error_indication(None, ran, e.cause).await;
575 return true;
576 }
577 };
578 if self.ues.len() >= MAX_UE_CONTEXTS {
579 Log::new(Some(&self.status_tx)).warn(format!(
580 "NGAP Initial UE Message from {} refused: {MAX_UE_CONTEXTS} UE contexts already \
581 held on this association (decision=fail_closed_ue_cap)",
582 self.remote_addr
583 ));
584 self.send_error_indication(
585 None,
586 Some(ue.ran_ue_ngap_id),
587 Cause::misc(codec::MISC_CONTROL_PROCESSING_OVERLOAD),
588 )
589 .await;
590 return true;
591 }
592 let amf_id = self.next_amf_ue_ngap_id;
593 self.next_amf_ue_ngap_id = (self.next_amf_ue_ngap_id + 1) % (codec::MAX_AMF_UE_NGAP_ID + 1);
594 let initial_nas = nas::message_type(&ue.nas);
595 self.ues.insert(
596 amf_id,
597 Ue {
598 ran_ue_ngap_id: ue.ran_ue_ngap_id,
599 initial_nas,
600 },
601 );
602 self.set_connection_state().await;
603 let data = json!({
604 "amf_ue_ngap_id": amf_id,
605 "ran_ue_ngap_id": ue.ran_ue_ngap_id,
606 "nas": nas::decode(&ue.nas),
607 "user_location": ue.user_location,
608 "rrc_establishment_cause": ue.rrc_establishment_cause,
609 "five_g_s_tmsi": ue.five_g_s_tmsi,
610 "uninterpreted_ies": ue.uninterpreted,
611 "gnb": self.gnb.clone(),
612 "transport": self.transport.label(),
613 });
614 let event = Event::new(&NGAP_INITIAL_UE_MESSAGE_EVENT, data);
615 let outcome = self.call_llm_and_send(&event).await;
616 self.settle_ue_outcome(&event.event_type.id, amf_id, outcome, true)
617 .await
618 }
619
620 async fn on_uplink_nas(&mut self, pdu: &codec::Pdu) -> bool {
621 let t = match codec::parse_nas_transport(pdu) {
622 Ok(t) => t,
623 Err(e) => {
624 Log::new(Some(&self.status_tx)).warn(format!(
625 "NGAP Uplink NAS Transport from {} refused before the model: {e}",
626 self.remote_addr
627 ));
628 self.send_error_indication(None, None, e.cause).await;
629 return true;
630 }
631 };
632 match self.ues.get(&t.amf_ue_ngap_id) {
633 None => {
634 Log::new(Some(&self.status_tx)).warn(format!(
635 "NGAP Uplink NAS Transport from {} names unknown AMF UE NGAP ID {}",
636 self.remote_addr, t.amf_ue_ngap_id
637 ));
638 self.send_error_indication(
639 Some(t.amf_ue_ngap_id),
640 Some(t.ran_ue_ngap_id),
641 Cause::radio(codec::RADIO_UNKNOWN_LOCAL_UE_NGAP_ID),
642 )
643 .await;
644 return true;
645 }
646 Some(ue) if ue.ran_ue_ngap_id != t.ran_ue_ngap_id => {
647 Log::new(Some(&self.status_tx)).warn(format!(
648 "NGAP Uplink NAS Transport from {} pairs AMF UE NGAP ID {} with RAN UE NGAP \
649 ID {} (the context holds {})",
650 self.remote_addr, t.amf_ue_ngap_id, t.ran_ue_ngap_id, ue.ran_ue_ngap_id
651 ));
652 self.send_error_indication(
653 Some(t.amf_ue_ngap_id),
654 Some(t.ran_ue_ngap_id),
655 Cause::radio(codec::RADIO_INCONSISTENT_REMOTE_UE_NGAP_ID),
656 )
657 .await;
658 return true;
659 }
660 Some(_) => {}
661 }
662 let data = json!({
663 "amf_ue_ngap_id": t.amf_ue_ngap_id,
664 "ran_ue_ngap_id": t.ran_ue_ngap_id,
665 "nas": nas::decode(&t.nas),
666 "user_location": t.user_location,
667 "uninterpreted_ies": t.uninterpreted,
668 "transport": self.transport.label(),
669 });
670 let event = Event::new(&NGAP_UPLINK_NAS_EVENT, data);
671 let outcome = self.call_llm_and_send(&event).await;
672 self.settle_ue_outcome(&event.event_type.id, t.amf_ue_ngap_id, outcome, false)
673 .await
674 }
675
676 async fn settle_ue_outcome(
679 &mut self,
680 event_id: &str,
681 amf_id: u64,
682 outcome: HandlerOutcome,
683 must_answer: bool,
684 ) -> bool {
685 match outcome {
686 HandlerOutcome::Ran { ref sent, closed }
687 if sent.iter().any(|s| s.amf_ue_ngap_id == Some(amf_id)) =>
688 {
689 !closed
690 }
691 HandlerOutcome::Ran { closed, .. } if !must_answer => {
692 debug!(
693 "NGAP {event_id} decision=model_silent amf_ue_ngap_id={amf_id}: nothing sent"
694 );
695 !closed
696 }
697 HandlerOutcome::Ran { closed, .. } => {
698 Log::new(Some(&self.status_tx)).warn(format!(
699 "NGAP {event_id} produced no answer for UE {amf_id} (decision=model_silent); \
700 rejecting"
701 ));
702 self.fail_closed_ue(amf_id, None).await;
703 !closed
704 }
705 HandlerOutcome::Failed(failure) => {
706 Log::new(Some(&self.status_tx)).warn(format!(
707 "NGAP {event_id} could not be evaluated for UE {amf_id} \
708 (decision=fail_closed_llm_error, {}); rejecting",
709 failure_category(failure)
710 ));
711 self.fail_closed_ue(amf_id, Some(failure)).await;
712 true
713 }
714 HandlerOutcome::NoPolicy => {
715 Log::new(Some(&self.status_tx)).warn(format!(
716 "NGAP {event_id} for UE {amf_id} (decision=no_policy_configured): nothing \
717 decides what this AMF tells UEs; rejecting"
718 ));
719 self.fail_closed_ue(amf_id, None).await;
720 true
721 }
722 }
723 }
724
725 async fn fail_closed_ue(&mut self, amf_id: u64, failure: Option<WireFailure>) {
728 let Some(ue) = self.ues.get(&amf_id) else {
729 return;
730 };
731 let ran = ue.ran_ue_ngap_id;
732 let overloaded = failure.map(|f| f.is_overloaded()).unwrap_or(false);
733 let nas_cause = if overloaded {
734 nas::CAUSE_CONGESTION
735 } else {
736 nas::CAUSE_PROTOCOL_ERROR_UNSPECIFIED
737 };
738 let reject = match ue.initial_nas {
739 Some(nas::REGISTRATION_REQUEST) => Some("registration_reject"),
740 Some(nas::SERVICE_REQUEST) => Some("service_reject"),
741 _ => None,
742 };
743 if let Some(message) = reject {
744 let pdu = nas::encode(
745 &json!({"message": message, "cause": nas_cause}),
746 nas::DOWNLINK_MESSAGES,
747 )
748 .map_err(codec::EncodeError)
749 .and_then(|n| codec::downlink_nas_transport(amf_id, ran, &n));
750 self.send_encoded("Downlink NAS Transport (reject)", pdu)
751 .await;
752 }
753 let cause = if overloaded {
754 Cause::misc(codec::MISC_CONTROL_PROCESSING_OVERLOAD)
755 } else {
756 Cause::nas(codec::NAS_UNSPECIFIED)
757 };
758 self.send_encoded(
759 "UE Context Release Command",
760 codec::ue_context_release_command(amf_id, Some(ran), &cause),
761 )
762 .await;
763 }
764
765 async fn on_error_indication(&mut self, pdu: &codec::Pdu) -> bool {
766 let mut data = match codec::parse_cause_message(pdu) {
767 Ok(v) => v,
768 Err(e) => {
769 warn!(
770 "NGAP Error Indication from {} did not decode: {e}",
771 self.remote_addr
772 );
773 return true;
774 }
775 };
776 Log::new(Some(&self.status_tx)).warn(format!(
777 "NGAP Error Indication from {}: {}",
778 self.remote_addr,
779 data.get("cause")
780 .map(|c| c.to_string())
781 .unwrap_or_else(|| "no cause".into())
782 ));
783 data["transport"] = json!(self.transport.label());
784 let event = Event::new(&NGAP_ERROR_INDICATION_EVENT, data);
785 match self.call_llm_and_send(&event).await {
786 HandlerOutcome::Failed(f) => warn!(
787 "NGAP ngap_error_indication decision=fail_closed_llm_error ({}); nothing sent — \
788 an Error Indication needs no answer",
789 failure_category(f)
790 ),
791 HandlerOutcome::Ran { closed: true, .. } => return false,
792 _ => {}
793 }
794 true
795 }
796
797 async fn on_ng_reset(&mut self, pdu: &codec::Pdu) -> bool {
798 let mut budget = codec::Budget::default();
799 let partial = match pdu
800 .require(codec::IE_RESET_TYPE)
801 .and_then(|ie| codec::decode_reset_type(ie, &mut budget))
802 {
803 Ok(p) => p,
804 Err(e) => {
805 self.send_error_indication(None, None, e.cause).await;
806 return true;
807 }
808 };
809 let cleared = match &partial {
811 None => {
812 let n = self.ues.len();
813 self.ues.clear();
814 n
815 }
816 Some(items) => {
817 let before = self.ues.len();
818 for (amf, ran) in items {
819 match (amf, ran) {
820 (Some(a), _) => {
821 self.ues.remove(a);
822 }
823 (None, Some(r)) => self.ues.retain(|_, u| u.ran_ue_ngap_id != *r),
824 (None, None) => {}
825 }
826 }
827 before - self.ues.len()
828 }
829 };
830 self.set_connection_state().await;
831 Log::new(Some(&self.status_tx)).info(format!(
832 "NGAP NG Reset from {}: {cleared} UE contexts cleared",
833 self.remote_addr
834 ));
835 self.send_encoded(
836 "NG Reset Acknowledge",
837 codec::ng_reset_acknowledge(partial.as_deref()),
838 )
839 .await;
840 true
841 }
842
843 async fn on_release_complete(&mut self, pdu: &codec::Pdu) -> bool {
844 if let Some(amf) = pdu
845 .ie(codec::IE_AMF_UE_NGAP_ID)
846 .and_then(|ie| codec::decode_amf_ue_ngap_id(ie).ok())
847 {
848 if self.ues.remove(&amf).is_some() {
849 info!("NGAP UE context {amf} released by {}", self.remote_addr);
850 self.set_connection_state().await;
851 }
852 }
853 true
854 }
855
856 async fn call_llm_and_send(&mut self, event: &Event) -> HandlerOutcome {
859 if !operator_wants_dynamic(&self.app_state, self.server_id, &event.event_type.id).await {
860 return HandlerOutcome::NoPolicy;
861 }
862 let result = match call_llm(
863 &self.llm_client,
864 &self.app_state,
865 self.server_id,
866 Some(self.connection_id),
867 event,
868 &*self.protocol,
869 )
870 .await
871 {
872 Ok(r) => r,
873 Err(e) => {
874 let failure = WireFailure::classify(&e);
875 Log::new(Some(&self.status_tx)).error(format!(
877 "NGAP {} decision=fail_closed_llm_error category={:?} peer={}: {:#}",
878 event.event_type.id, failure, self.remote_addr, e
879 ));
880 return HandlerOutcome::Failed(failure);
881 }
882 };
883 let mut sent = Vec::new();
884 let mut closed = false;
885 let mut queue: Vec<ActionResult> = result.protocol_results;
886 queue.reverse();
887 while let Some(item) = queue.pop() {
888 match item {
889 ActionResult::Multiple(inner) => {
890 for nested in inner.into_iter().rev() {
891 queue.push(nested);
892 }
893 }
894 ActionResult::Output(bytes) => {
895 if let Ok(p) = codec::decode_pdu(&bytes) {
896 sent.push(Sent {
897 kind: p.kind,
898 code: p.procedure_code,
899 amf_ue_ngap_id: p
900 .ie(codec::IE_AMF_UE_NGAP_ID)
901 .and_then(|ie| codec::decode_amf_ue_ngap_id(ie).ok())
902 .or_else(|| {
903 p.ie(codec::IE_UE_NGAP_IDS).and_then(|ie| {
904 codec::decode_ue_ngap_ids(ie, &mut codec::Budget::default())
905 .ok()
906 .map(|(a, _)| a)
907 })
908 }),
909 });
910 }
911 self.send(bytes).await;
912 }
913 ActionResult::CloseConnection => {
914 self.writer.lock().await.close();
915 closed = true;
916 }
917 ActionResult::WaitForMore | ActionResult::NoAction => {}
918 other => debug!("NGAP ignoring action result {other:?}"),
919 }
920 }
921 HandlerOutcome::Ran { sent, closed }
922 }
923
924 async fn send(&self, bytes: Vec<u8>) {
925 let ok = self.writer.lock().await.send(&bytes).await.is_ok();
926 if !ok {
927 debug!(
928 "NGAP write failed on association {}; peer has gone",
929 self.connection_id
930 );
931 return;
932 }
933 self.app_state
934 .update_connection_stats(
935 self.server_id,
936 self.connection_id,
937 None,
938 Some(bytes.len() as u64),
939 None,
940 Some(1),
941 )
942 .await;
943 }
944
945 async fn send_encoded(&self, what: &str, encoded: codec::EncodeResult<Vec<u8>>) {
946 match encoded {
947 Ok(bytes) => self.send(bytes).await,
948 Err(e) => error!(
949 "NGAP decision=fail_closed_encode: refusing to send {what} to {}: {e}",
950 self.remote_addr
951 ),
952 }
953 }
954
955 async fn send_error_indication(&self, amf: Option<u64>, ran: Option<u64>, cause: Cause) {
956 Log::new(Some(&self.status_tx)).warn(format!(
957 "NGAP Error Indication ({}) to {}",
958 cause.label(),
959 self.remote_addr
960 ));
961 self.send_encoded(
962 "Error Indication",
963 codec::error_indication(amf, ran, Some(&cause)),
964 )
965 .await;
966 }
967
968 async fn set_connection_state(&self) {
969 let value = json!({
970 "state": if self.gnb.is_some() { "NG Setup complete" } else { "awaiting NG Setup" },
971 "gnb": self.gnb,
972 "ue_contexts": self.ues.len(),
973 "transport": self.transport.label(),
974 });
975 let connection_id = self.connection_id;
976 self.app_state
977 .with_server_mut(self.server_id, |server| {
978 if let Some(c) = server.connections.get_mut(&connection_id) {
979 c.protocol_info =
980 crate::state::server::ProtocolConnectionInfo::new(value.clone());
981 }
982 })
983 .await;
984 }
985}
986
987fn failure_category(failure: WireFailure) -> &'static str {
988 if failure.is_overloaded() {
989 "resource"
990 } else {
991 "unavailable"
992 }
993}
994
995async fn operator_wants_dynamic(state: &AppState, server_id: ServerId, event_id: &str) -> bool {
998 state
999 .with_server_mut(server_id, |server| {
1000 let has_instruction = !server.instruction.trim().is_empty();
1001 let has_handler = server
1002 .event_handler_config
1003 .as_ref()
1004 .map(|c| c.find_handler(event_id).is_some())
1005 .unwrap_or(false);
1006 has_instruction || has_handler
1007 })
1008 .await
1009 .unwrap_or(false)
1010}