1pub mod actions;
59pub mod ber;
60pub mod cap;
61pub mod codec;
62pub mod gsm;
63pub mod map;
64pub mod sccp;
65pub mod ss7;
66pub mod ss7_actions;
67pub mod tcap;
68pub mod transport;
69
70use anyhow::{anyhow, Result};
71use std::net::SocketAddr;
72use std::sync::Arc;
73use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
74use tokio::net::{TcpListener, TcpStream};
75use tokio::sync::{mpsc, Mutex};
76use tracing::{debug, error, info, trace, warn};
77use transport::{Reader, Writer};
78
79use crate::llm::action_helper::call_llm;
80use crate::llm::actions::protocol_trait::ActionResult;
81use crate::llm::ollama_client::OllamaClient;
82use crate::logging::emit::Log;
83use crate::protocol::Event;
84use crate::server::connection::ConnectionId;
85use crate::server::M3uaProtocol;
86use crate::state::app_state::AppState;
87use crate::state::ServerId;
88use crate::utils::WireFailure;
89
90use actions::{
91 M3UA_ASP_ACTIVE_EVENT, M3UA_ASP_DOWN_EVENT, M3UA_ASP_UP_EVENT, M3UA_DATA_EVENT,
92 M3UA_ERROR_EVENT,
93};
94
95const IPPROTO_SCTP: i32 = 132;
99
100const FIRST_MESSAGE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
106
107const IDLE_BETWEEN_MESSAGES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
118
119const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
121
122const CONNECTION_CAP_REFUSAL: &[u8] = &[
134 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, ];
139
140pub struct M3uaServer;
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum M3uaTransport {
146 Sctp,
148 Tcp,
151}
152
153impl M3uaTransport {
154 pub fn parse(value: &str) -> Result<Self> {
155 match value.trim().to_ascii_lowercase().as_str() {
156 "sctp" => Ok(Self::Sctp),
157 "tcp" => Ok(Self::Tcp),
158 other => Err(anyhow!(
159 "transport must be \"sctp\" (the transport RFC 4666 defines) or \"tcp\" \
160 (non-standard, for local protocol work only), got {other:?}"
161 )),
162 }
163 }
164
165 pub fn as_str(self) -> &'static str {
166 match self {
167 Self::Sctp => "sctp",
168 Self::Tcp => "tcp",
169 }
170 }
171
172 pub fn label(self) -> &'static str {
178 match self {
179 Self::Sctp => "sctp (RFC 4666)",
180 Self::Tcp => "tcp (NON-STANDARD lab transport, not SIGTRAN)",
181 }
182 }
183}
184
185pub async fn bind_listener(addr: SocketAddr, transport: M3uaTransport) -> Result<TcpListener> {
190 match transport {
191 M3uaTransport::Tcp => {
192 crate::server::socket_helpers::create_reusable_tcp_listener(addr).await
193 }
194 M3uaTransport::Sctp => bind_sctp_listener(addr),
195 }
196}
197
198fn bind_sctp_listener(addr: SocketAddr) -> Result<TcpListener> {
205 use socket2::{Domain, Protocol, Socket, Type};
206
207 let domain = if addr.is_ipv4() {
208 Domain::IPV4
209 } else {
210 Domain::IPV6
211 };
212
213 let socket =
214 Socket::new(domain, Type::STREAM, Some(Protocol::from(IPPROTO_SCTP))).map_err(|e| {
215 anyhow!(
216 "M3UA cannot open an SCTP socket on this host: {e}. M3UA's transport is SCTP \
217 (RFC 4666 section 1.4.1, IANA port 2905) and this operating system has no SCTP \
218 stack available to this process — macOS ships neither kernel support nor \
219 headers for SCTP, and a Linux kernel without the sctp module fails the same \
220 way. NetGet refuses to start rather than pretend it is speaking SIGTRAN. \
221 Either run this server on a host with SCTP, or, for local protocol work only, \
222 pass the startup parameter transport=\"tcp\" — a NON-STANDARD framing that \
223 exercises the M3UA layer and that no real SIGTRAN peer speaks."
224 )
225 })?;
226
227 socket
228 .set_reuse_address(true)
229 .map_err(|e| anyhow!("M3UA could not set SO_REUSEADDR on the SCTP socket: {e}"))?;
230 socket
231 .bind(&addr.into())
232 .map_err(|e| anyhow!("M3UA could not bind the SCTP socket to {addr}: {e}"))?;
233 socket
234 .listen(128)
235 .map_err(|e| anyhow!("M3UA could not listen on the SCTP socket at {addr}: {e}"))?;
236 socket
237 .set_nonblocking(true)
238 .map_err(|e| anyhow!("M3UA could not put the SCTP socket in non-blocking mode: {e}"))?;
239
240 let std_listener: std::net::TcpListener = socket.into();
241 TcpListener::from_std(std_listener)
242 .map_err(|e| anyhow!("M3UA could not register the SCTP listener with tokio: {e}"))
243}
244
245#[derive(Clone, Copy, Debug)]
247pub struct M3uaConfig {
248 pub transport: M3uaTransport,
249 pub routing_context: Option<u32>,
251 pub network_appearance: Option<u32>,
253}
254
255impl M3uaConfig {
256 fn from_params(params: Option<&crate::protocol::StartupParams>) -> Result<Self> {
262 let Some(params) = params else {
263 return Ok(Self {
264 transport: M3uaTransport::Sctp,
265 routing_context: None,
266 network_appearance: None,
267 });
268 };
269
270 let transport = match params.get_optional_string("transport")? {
271 Some(s) => M3uaTransport::parse(&s)?,
272 None => M3uaTransport::Sctp,
273 };
274
275 Ok(Self {
276 transport,
277 routing_context: params.get_optional_u32("routing_context")?,
278 network_appearance: params.get_optional_u32("network_appearance")?,
279 })
280 }
281}
282
283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285pub enum AspState {
286 Down,
287 Inactive,
288 Active,
289}
290
291impl AspState {
292 fn as_str(self) -> &'static str {
293 match self {
294 Self::Down => "ASP-DOWN",
295 Self::Inactive => "ASP-INACTIVE",
296 Self::Active => "ASP-ACTIVE",
297 }
298 }
299}
300
301impl M3uaServer {
302 pub async fn spawn_with_llm_actions(
308 listen_addr: SocketAddr,
309 llm_client: OllamaClient,
310 app_state: Arc<AppState>,
311 status_tx: mpsc::UnboundedSender<String>,
312 server_id: ServerId,
313 startup_params: Option<crate::protocol::StartupParams>,
314 ) -> Result<SocketAddr> {
315 let config = M3uaConfig::from_params(startup_params.as_ref())?;
316 let listener = bind_listener(listen_addr, config.transport).await?;
317 let local_addr = listener.local_addr()?;
318
319 Log::new(Some(&status_tx)).info(format!(
320 "M3UA SGP listening on {} over {}",
321 local_addr,
322 config.transport.label()
323 ));
324 if config.transport == M3uaTransport::Tcp {
325 Log::new(Some(&status_tx)).warn(
326 "M3UA is running over TCP. This is NOT SIGTRAN: RFC 4666 defines M3UA over \
327 SCTP only, and no real ASP will connect over TCP. Use it for local protocol \
328 work and nothing else.",
329 );
330 }
331
332 let protocol = Arc::new(M3uaProtocol::new());
333 let task_registrar = app_state.clone();
334
335 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
336 let accept_handle = tokio::spawn(async move {
337 loop {
338 match crate::server::accept_bounded::accept_bounded(
339 &listener,
340 &limiter,
341 CONNECTION_CAP_REFUSAL,
342 "M3UA",
343 Some(&status_tx),
344 )
345 .await
346 {
347 Ok((stream, remote_addr, permit)) => {
348 let connection_id =
349 ConnectionId::new(app_state.get_next_unified_id().await);
350 Log::new(Some(&status_tx)).info(format!(
351 "M3UA association {} from {} ({})",
352 connection_id,
353 remote_addr,
354 config.transport.as_str()
355 ));
356
357 register_connection(
358 &app_state,
359 server_id,
360 connection_id,
361 remote_addr,
362 config.transport,
363 )
364 .await;
365 let _ = status_tx.send("__UPDATE_UI__".to_string());
366
367 let llm_clone = llm_client.clone();
368 let state_clone = app_state.clone();
369 let status_clone = status_tx.clone();
370 let protocol_clone = protocol.clone();
371
372 let task_owner = app_state.clone();
374 task_owner
375 .spawn_server_task(server_id, async move {
376 let _permit = permit;
379 if let Err(e) = run_session(
380 stream,
381 connection_id,
382 server_id,
383 remote_addr,
384 llm_clone,
385 state_clone.clone(),
386 status_clone.clone(),
387 protocol_clone,
388 config,
389 )
390 .await
391 {
392 Log::new(Some(&status_clone))
393 .error(format!("M3UA association error: {}", e));
394 }
395
396 state_clone
397 .remove_peer_handle(server_id, connection_id.as_u32())
398 .await;
399 state_clone
400 .close_connection_on_server(server_id, connection_id)
401 .await;
402 Log::new(Some(&status_clone))
403 .info(format!("M3UA association {} closed", connection_id));
404 let _ = status_clone.send("__UPDATE_UI__".to_string());
405 })
406 .await;
407 }
408 Err(e) => {
409 Log::new(Some(&status_tx))
410 .error(format!("Failed to accept M3UA association: {}", e));
411 break;
412 }
413 }
414 }
415 });
416
417 task_registrar
419 .register_server_task(server_id, accept_handle)
420 .await;
421
422 Ok(local_addr)
423 }
424}
425
426async fn register_connection(
427 app_state: &AppState,
428 server_id: ServerId,
429 connection_id: ConnectionId,
430 remote_addr: SocketAddr,
431 transport: M3uaTransport,
432) {
433 use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
434 let now = crate::utils::clock::Instant::now();
435 app_state
436 .add_connection_to_server(
437 server_id,
438 ConnectionState {
439 id: connection_id,
440 remote_addr,
441 local_addr: remote_addr,
442 bytes_sent: 0,
443 bytes_received: 0,
444 packets_sent: 0,
445 packets_received: 0,
446 last_activity: now,
447 status: ConnectionStatus::Active,
448 status_changed_at: now,
449 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
450 "asp_state": AspState::Down.as_str(),
451 "transport": transport.label(),
454 })),
455 },
456 )
457 .await;
458}
459
460struct M3uaSession {
462 connection_id: ConnectionId,
463 server_id: ServerId,
464 remote_addr: SocketAddr,
465 llm_client: OllamaClient,
466 app_state: Arc<AppState>,
467 status_tx: mpsc::UnboundedSender<String>,
468 protocol: Arc<M3uaProtocol>,
469 config: M3uaConfig,
470 state: AspState,
471 asp_identifier: Option<u32>,
474 writer: Arc<Mutex<Writer>>,
475 engine: ss7::Engine,
477}
478
479pub enum Incoming {
481 Message(codec::Message),
482 HeaderError(codec::WireError),
485 BodyError(codec::WireError),
487 Eof,
488}
489
490#[allow(clippy::too_many_arguments)]
491async fn run_session(
492 stream: TcpStream,
493 connection_id: ConnectionId,
494 server_id: ServerId,
495 remote_addr: SocketAddr,
496 llm_client: OllamaClient,
497 app_state: Arc<AppState>,
498 status_tx: mpsc::UnboundedSender<String>,
499 protocol: Arc<M3uaProtocol>,
500 config: M3uaConfig,
501) -> Result<()> {
502 let (mut reader, writer) = transport::split(stream, config.transport == M3uaTransport::Sctp);
506 let writer = Arc::new(Mutex::new(writer));
507
508 let peer_rx = crate::server::peer_support::register_peer_channel(
511 &app_state,
512 server_id,
513 connection_id.as_u32(),
514 )
515 .await;
516 crate::server::peer_support::spawn_peer_command_task(
517 peer_rx,
518 Arc::new(M3uaProtocol::for_peer_commands()),
519 app_state.clone(),
520 server_id,
521 connection_id.as_u32(),
522 writer.clone(),
523 status_tx.clone(),
524 );
525
526 let mut session = M3uaSession {
527 connection_id,
528 server_id,
529 remote_addr,
530 llm_client,
531 app_state,
532 status_tx,
533 protocol,
534 config,
535 state: AspState::Down,
536 asp_identifier: None,
537 writer,
538 engine: ss7::Engine::new(),
539 };
540
541 session.run(&mut reader).await
542}
543
544impl M3uaSession {
545 async fn run(&mut self, reader: &mut Reader) -> Result<()> {
546 debug!(
547 "M3UA association {} open, ASP in {}",
548 self.connection_id,
549 self.state.as_str()
550 );
551
552 let mut received_one = false;
553
554 loop {
555 let read_timeout = if received_one {
563 IDLE_BETWEEN_MESSAGES_TIMEOUT
564 } else {
565 FIRST_MESSAGE_READ_TIMEOUT
566 };
567 let (incoming, bytes_in) = read_message(reader, read_timeout).await;
568 if !matches!(incoming, Incoming::Eof) {
569 received_one = true;
570 }
571 if bytes_in > 0 {
572 self.app_state
573 .update_connection_stats(
574 self.server_id,
575 self.connection_id,
576 Some(bytes_in as u64),
577 None,
578 Some(1),
579 None,
580 )
581 .await;
582 }
583
584 match incoming {
585 Incoming::Eof => {
586 debug!("M3UA association {} closed by peer", self.connection_id);
587 break;
588 }
589 Incoming::HeaderError(e) => {
590 Log::new(Some(&self.status_tx)).warn(format!(
591 "M3UA header rejected from {}: {}",
592 self.remote_addr, e
593 ));
594 self.send_error(e.error_code).await;
595 break;
596 }
597 Incoming::BodyError(e) => {
598 Log::new(Some(&self.status_tx)).warn(format!(
599 "M3UA message from {} rejected: {}",
600 self.remote_addr, e
601 ));
602 self.send_error(e.error_code).await;
603 }
604 Incoming::Message(message) => {
605 if !self.dispatch(message).await? {
606 break;
607 }
608 }
609 }
610 }
611 Ok(())
612 }
613
614 async fn dispatch(&mut self, message: codec::Message) -> Result<bool> {
616 trace!(
617 "M3UA {} from {} in {}",
618 message.name(),
619 self.remote_addr,
620 self.state.as_str()
621 );
622
623 match (message.class, message.msg_type) {
624 (codec::CLASS_ASPSM, codec::ASPSM_ASPUP) => self.on_asp_up(&message).await,
625 (codec::CLASS_ASPSM, codec::ASPSM_ASPDN) => self.on_asp_down(&message).await,
626 (codec::CLASS_ASPSM, codec::ASPSM_BEAT) => {
627 let heartbeat = message
632 .param(codec::TAG_HEARTBEAT_DATA)
633 .map(|p| p.value.clone());
634 trace!("M3UA BEAT from {} -> BEAT ACK", self.remote_addr);
635 match codec::beat_ack(heartbeat.as_deref()) {
641 Ok(bytes) => self.send(bytes).await,
642 Err(e) => {
643 error!(
644 "M3UA decision=fail_closed_encode: BEAT from {} carries Heartbeat \
645 Data that cannot be echoed within the message ceiling: {}",
646 self.remote_addr, e
647 );
648 self.send_error(codec::ERR_PROTOCOL_ERROR).await;
649 }
650 }
651 Ok(true)
652 }
653 (codec::CLASS_ASPTM, codec::ASPTM_ASPAC) => self.on_asp_active(&message).await,
654 (codec::CLASS_ASPTM, codec::ASPTM_ASPIA) => {
655 self.state = AspState::Inactive;
658 self.set_connection_state().await;
659 Log::new(Some(&self.status_tx)).info(format!(
660 "M3UA ASPIA from {}, ASP is now {}",
661 self.remote_addr,
662 self.state.as_str()
663 ));
664 self.send_encoded(
665 "ASPIA ACK",
666 codec::aspia_ack(message.param_u32(codec::TAG_ROUTING_CONTEXT)),
667 )
668 .await;
669 Ok(true)
670 }
671 (codec::CLASS_TRANSFER, codec::TRANSFER_DATA) => self.on_data(&message).await,
672 (codec::CLASS_MGMT, codec::MGMT_ERR) => self.on_error_received(&message).await,
673 (codec::CLASS_MGMT, codec::MGMT_NTFY) => {
674 Log::new(Some(&self.status_tx)).info(format!(
676 "M3UA NTFY from {} status=0x{:08x}",
677 self.remote_addr,
678 message.param_u32(codec::TAG_STATUS).unwrap_or(0)
679 ));
680 Ok(true)
681 }
682 (codec::CLASS_RKM, _) => {
683 Log::new(Some(&self.status_tx)).warn(format!(
688 "M3UA {} from {} refused: NetGet implements no routing key table \
689 (dynamic registration is storage; configure routing_context instead)",
690 message.name(),
691 self.remote_addr
692 ));
693 self.send_error(codec::ERR_UNSUPPORTED_MESSAGE_TYPE).await;
694 Ok(true)
695 }
696 (codec::CLASS_SSNM, _) => {
697 warn!(
699 "M3UA {} from {} is an SGP-to-ASP message; refusing",
700 message.name(),
701 self.remote_addr
702 );
703 self.send_error(codec::ERR_UNEXPECTED_MESSAGE).await;
704 Ok(true)
705 }
706 (class, msg_type) => {
707 let known_class = matches!(
708 class,
709 codec::CLASS_MGMT
710 | codec::CLASS_TRANSFER
711 | codec::CLASS_SSNM
712 | codec::CLASS_ASPSM
713 | codec::CLASS_ASPTM
714 | codec::CLASS_RKM
715 );
716 let code = if known_class {
717 codec::ERR_UNSUPPORTED_MESSAGE_TYPE
718 } else {
719 codec::ERR_UNSUPPORTED_MESSAGE_CLASS
720 };
721 Log::new(Some(&self.status_tx)).warn(format!(
722 "M3UA class {} ({}) type {} from {} is not supported",
723 class,
724 codec::class_name(class),
725 msg_type,
726 self.remote_addr
727 ));
728 self.send_error(code).await;
729 Ok(true)
730 }
731 }
732 }
733
734 async fn on_asp_up(&mut self, message: &codec::Message) -> Result<bool> {
739 self.asp_identifier = message.param_u32(codec::TAG_ASP_IDENTIFIER);
740
741 if self.state != AspState::Down {
745 debug!(
746 "M3UA duplicate ASPUP from {} while {}; re-acknowledging",
747 self.remote_addr,
748 self.state.as_str()
749 );
750 self.send_encoded("ASPUP ACK", codec::aspup_ack(None)).await;
751 return Ok(true);
752 }
753
754 let info_string = message
755 .param(codec::TAG_INFO_STRING)
756 .map(|p| String::from_utf8_lossy(&p.value).to_string());
757
758 Log::new(Some(&self.status_tx)).info(format!(
759 "M3UA ASPUP from {} (asp_id={:?})",
760 self.remote_addr, self.asp_identifier
761 ));
762
763 let event = Event {
764 event_type: &M3UA_ASP_UP_EVENT,
765 data: serde_json::json!({
766 "connection_id": self.connection_id.to_string(),
767 "remote_addr": self.remote_addr.to_string(),
768 "transport": self.config.transport.label(),
769 "asp_identifier": self.asp_identifier,
770 "info_string": info_string,
771 "asp_state": self.state.as_str(),
772 }),
773 };
774
775 self.decide_admission(
776 &event,
777 (codec::CLASS_ASPSM, codec::ASPSM_ASPUP_ACK),
778 AspState::Inactive,
779 )
780 .await
781 }
782
783 async fn on_asp_active(&mut self, message: &codec::Message) -> Result<bool> {
784 if self.state == AspState::Down {
787 Log::new(Some(&self.status_tx)).warn(format!(
788 "M3UA ASPAC from {} while ASP-DOWN; refusing without consulting policy",
789 self.remote_addr
790 ));
791 self.send_error(codec::ERR_UNEXPECTED_MESSAGE).await;
792 return Ok(true);
793 }
794
795 let routing_context = message.param_u32(codec::TAG_ROUTING_CONTEXT);
796 if let Some(expected) = self.config.routing_context {
797 if let Some(offered) = routing_context {
798 if offered != expected {
799 Log::new(Some(&self.status_tx)).warn(format!(
800 "M3UA ASPAC from {} names routing context {} but this SGP serves {}",
801 self.remote_addr, offered, expected
802 ));
803 self.send_error(codec::ERR_INVALID_ROUTING_CONTEXT).await;
804 return Ok(true);
805 }
806 }
807 }
808
809 if self.state == AspState::Active {
811 debug!(
812 "M3UA duplicate ASPAC from {} while ASP-ACTIVE; re-acknowledging",
813 self.remote_addr
814 );
815 self.send_encoded(
816 "ASPAC ACK",
817 codec::aspac_ack(
818 message.param_u32(codec::TAG_TRAFFIC_MODE_TYPE),
819 routing_context,
820 None,
821 ),
822 )
823 .await;
824 return Ok(true);
825 }
826
827 let traffic_mode = message.param_u32(codec::TAG_TRAFFIC_MODE_TYPE);
828 Log::new(Some(&self.status_tx)).info(format!(
829 "M3UA ASPAC from {} (rc={:?}, traffic_mode={:?})",
830 self.remote_addr, routing_context, traffic_mode
831 ));
832
833 let event = Event {
834 event_type: &M3UA_ASP_ACTIVE_EVENT,
835 data: serde_json::json!({
836 "connection_id": self.connection_id.to_string(),
837 "remote_addr": self.remote_addr.to_string(),
838 "transport": self.config.transport.label(),
839 "asp_identifier": self.asp_identifier,
840 "routing_context": routing_context,
841 "traffic_mode": traffic_mode,
842 "traffic_mode_name": traffic_mode.map(traffic_mode_name),
843 "asp_state": self.state.as_str(),
844 }),
845 };
846
847 self.decide_admission(
848 &event,
849 (codec::CLASS_ASPTM, codec::ASPTM_ASPAC_ACK),
850 AspState::Active,
851 )
852 .await
853 }
854
855 async fn decide_admission(
862 &mut self,
863 event: &Event,
864 ack: (u8, u8),
865 next_state: AspState,
866 ) -> Result<bool> {
867 let event_id = event.event_type.id.clone();
868 let outcome = self.call_llm_and_send(event).await;
869
870 match outcome {
871 HandlerOutcome::Ran { ref sent, closed } if sent.contains(&ack) => {
872 self.state = next_state;
873 self.set_connection_state().await;
874 Log::new(Some(&self.status_tx)).info(format!(
875 "M3UA {} admitted {} to {}",
876 event_id,
877 self.remote_addr,
878 self.state.as_str()
879 ));
880 Ok(!closed)
881 }
882 HandlerOutcome::Ran { ref sent, closed }
883 if sent.contains(&(codec::CLASS_MGMT, codec::MGMT_ERR)) =>
884 {
885 info!(
889 "M3UA {} decision=model_reject peer={} asp stays {}",
890 event_id,
891 self.remote_addr,
892 self.state.as_str()
893 );
894 Ok(!closed)
895 }
896 HandlerOutcome::Ran { closed, .. } => {
897 Log::new(Some(&self.status_tx)).warn(format!(
901 "M3UA {} produced no acknowledgement for {} (decision=model_silent); \
902 refusing, ASP stays {}",
903 event_id,
904 self.remote_addr,
905 self.state.as_str()
906 ));
907 self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
908 .await;
909 Ok(!closed)
910 }
911 HandlerOutcome::Failed(failure) => {
912 Log::new(Some(&self.status_tx)).warn(format!(
917 "M3UA {} could not be evaluated for {} (decision=fail_closed_llm_error, \
918 {}); refusing, ASP stays {}",
919 event_id,
920 self.remote_addr,
921 failure_category(failure),
922 self.state.as_str()
923 ));
924 self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
925 .await;
926 Ok(true)
927 }
928 HandlerOutcome::NoPolicy => {
929 Log::new(Some(&self.status_tx)).warn(format!(
933 "M3UA {} refused for {} (decision=no_policy_configured): this server has \
934 no instruction and no event handler, so nothing decides which ASPs may \
935 join. Give the server an instruction, or a handler for {}.",
936 event_id, self.remote_addr, event_id
937 ));
938 self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
939 .await;
940 Ok(true)
941 }
942 }
943 }
944
945 async fn on_asp_down(&mut self, _message: &codec::Message) -> Result<bool> {
946 self.send_encoded("ASPDN ACK", codec::aspdn_ack()).await;
950 self.state = AspState::Down;
951 self.set_connection_state().await;
952 Log::new(Some(&self.status_tx)).info(format!(
953 "M3UA ASPDN from {}, ASP is now {}",
954 self.remote_addr,
955 self.state.as_str()
956 ));
957
958 let event = Event {
959 event_type: &M3UA_ASP_DOWN_EVENT,
960 data: serde_json::json!({
961 "connection_id": self.connection_id.to_string(),
962 "remote_addr": self.remote_addr.to_string(),
963 "transport": self.config.transport.label(),
964 "asp_identifier": self.asp_identifier,
965 "asp_state": self.state.as_str(),
966 }),
967 };
968 let outcome = self.call_llm_and_send(&event).await;
969 self.log_observational_outcome("m3ua_asp_down_received", outcome);
970 Ok(true)
971 }
972
973 async fn on_error_received(&mut self, message: &codec::Message) -> Result<bool> {
974 let code = message.param_u32(codec::TAG_ERROR_CODE).unwrap_or(0);
975 Log::new(Some(&self.status_tx)).warn(format!(
976 "M3UA ERR from {}: 0x{:02x} {}",
977 self.remote_addr,
978 code,
979 codec::error_code_name(code)
980 ));
981
982 let event = Event {
983 event_type: &M3UA_ERROR_EVENT,
984 data: serde_json::json!({
985 "connection_id": self.connection_id.to_string(),
986 "remote_addr": self.remote_addr.to_string(),
987 "transport": self.config.transport.label(),
988 "error_code": code,
989 "error_name": codec::error_code_name(code),
990 "routing_context": message.param_u32(codec::TAG_ROUTING_CONTEXT),
991 "asp_state": self.state.as_str(),
992 }),
993 };
994 let outcome = self.call_llm_and_send(&event).await;
995 self.log_observational_outcome("m3ua_error_received", outcome);
996 Ok(true)
997 }
998
999 async fn on_data(&mut self, message: &codec::Message) -> Result<bool> {
1004 if self.state != AspState::Active {
1005 Log::new(Some(&self.status_tx)).warn(format!(
1006 "M3UA DATA from {} while {}; refusing",
1007 self.remote_addr,
1008 self.state.as_str()
1009 ));
1010 self.send_error(codec::ERR_UNEXPECTED_MESSAGE).await;
1011 return Ok(true);
1012 }
1013
1014 let routing_context = message.param_u32(codec::TAG_ROUTING_CONTEXT);
1015 if let (Some(expected), Some(offered)) = (self.config.routing_context, routing_context) {
1016 if expected != offered {
1017 self.send_error(codec::ERR_INVALID_ROUTING_CONTEXT).await;
1018 return Ok(true);
1019 }
1020 }
1021 let network_appearance = message.param_u32(codec::TAG_NETWORK_APPEARANCE);
1022 if let (Some(expected), Some(offered)) =
1023 (self.config.network_appearance, network_appearance)
1024 {
1025 if expected != offered {
1026 self.send_error(codec::ERR_INVALID_NETWORK_APPEARANCE).await;
1027 return Ok(true);
1028 }
1029 }
1030
1031 let Some(parameter) = message.param(codec::TAG_PROTOCOL_DATA) else {
1032 Log::new(Some(&self.status_tx)).warn(format!(
1033 "M3UA DATA from {} carries no Protocol Data parameter",
1034 self.remote_addr
1035 ));
1036 self.send_error(codec::ERR_MISSING_PARAMETER).await;
1037 return Ok(true);
1038 };
1039 let protocol_data = match codec::ProtocolData::parse(¶meter.value) {
1040 Ok(pd) => pd,
1041 Err(e) => {
1042 Log::new(Some(&self.status_tx)).warn(format!(
1043 "M3UA DATA from {} has malformed Protocol Data: {}",
1044 self.remote_addr, e
1045 ));
1046 self.send_error(e.error_code).await;
1047 return Ok(true);
1048 }
1049 };
1050
1051 let now = crate::utils::clock::Instant::now();
1055 self.expire_dialogues(now).await;
1056 let mut sccp_note = None;
1057 match self
1058 .engine
1059 .on_data(&protocol_data, routing_context, network_appearance, now)
1060 {
1061 ss7::Inbound::NotSccp => {}
1062 ss7::Inbound::Undecoded(reason) => sccp_note = Some(reason),
1063 ss7::Inbound::Handled { replies, note } => {
1064 Log::new(Some(&self.status_tx)).info(format!(
1065 "M3UA SCCP from {} opc={} handled without the model: {}",
1066 self.remote_addr, protocol_data.opc, note
1067 ));
1068 for reply in replies {
1069 self.send_ss7(reply).await;
1070 }
1071 return Ok(true);
1072 }
1073 ss7::Inbound::Event(event) => return Ok(self.on_ss7_event(event).await),
1074 }
1075
1076 let (payload, encoding) = render_payload(&protocol_data.payload);
1077 info!(
1078 "M3UA DATA from {} opc={} dpc={} si={} ({}) sls={}",
1079 self.remote_addr,
1080 protocol_data.opc,
1081 protocol_data.dpc,
1082 protocol_data.si,
1083 codec::si_name(protocol_data.si),
1084 protocol_data.sls
1085 );
1086
1087 let event = Event {
1088 event_type: &M3UA_DATA_EVENT,
1089 data: serde_json::json!({
1090 "connection_id": self.connection_id.to_string(),
1091 "remote_addr": self.remote_addr.to_string(),
1092 "transport": self.config.transport.label(),
1093 "opc": protocol_data.opc,
1094 "dpc": protocol_data.dpc,
1095 "si": protocol_data.si,
1096 "si_name": codec::si_name(protocol_data.si),
1097 "ni": protocol_data.ni,
1098 "mp": protocol_data.mp,
1099 "sls": protocol_data.sls,
1100 "payload": payload,
1101 "encoding": encoding,
1104 "payload_bytes": protocol_data.payload.len(),
1105 "routing_context": routing_context,
1106 "network_appearance": network_appearance,
1107 "correlation_id": message.param_u32(codec::TAG_CORRELATION_ID),
1108 "sccp_note": sccp_note,
1109 }),
1110 };
1111
1112 let outcome = self.call_llm_and_send(&event).await;
1113 match outcome {
1114 HandlerOutcome::Ran { closed, .. } => Ok(!closed),
1115 HandlerOutcome::Failed(failure) => {
1120 Log::new(Some(&self.status_tx)).warn(format!(
1121 "M3UA m3ua_data_received could not be evaluated for {} \
1122 (decision=fail_closed_llm_error, {}); answering ERR",
1123 self.remote_addr,
1124 failure_category(failure)
1125 ));
1126 self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
1127 .await;
1128 Ok(true)
1129 }
1130 HandlerOutcome::NoPolicy => {
1131 Log::new(Some(&self.status_tx)).warn(format!(
1134 "M3UA m3ua_data_received refused for {} (decision=no_policy_configured)",
1135 self.remote_addr
1136 ));
1137 self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
1138 .await;
1139 Ok(true)
1140 }
1141 }
1142 }
1143
1144 fn log_observational_outcome(&self, event_id: &str, outcome: HandlerOutcome) {
1150 match outcome {
1151 HandlerOutcome::Failed(failure) => Log::new(Some(&self.status_tx)).warn(format!(
1152 "M3UA {} could not be evaluated for {} (decision=fail_closed_llm_error, {}); \
1153 nothing sent",
1154 event_id,
1155 self.remote_addr,
1156 failure_category(failure)
1157 )),
1158 HandlerOutcome::NoPolicy => debug!(
1159 "M3UA {} peer={}: no operator policy configured, no LLM call",
1160 event_id, self.remote_addr
1161 ),
1162 HandlerOutcome::Ran { ref sent, .. } if sent.is_empty() => debug!(
1163 "M3UA {} decision=model_silent peer={}: nothing sent",
1164 event_id, self.remote_addr
1165 ),
1166 HandlerOutcome::Ran { .. } => debug!(
1167 "M3UA {} peer={}: handler answered",
1168 event_id, self.remote_addr
1169 ),
1170 }
1171 }
1172
1173 async fn call_llm_and_send(&mut self, event: &Event) -> HandlerOutcome {
1175 if !operator_wants_dynamic(&self.app_state, self.server_id, &event.event_type.id).await {
1176 debug!(
1177 "M3UA {}: no operator policy configured, no LLM call",
1178 event.event_type.id
1179 );
1180 return HandlerOutcome::NoPolicy;
1181 }
1182
1183 let result = match call_llm(
1184 &self.llm_client,
1185 &self.app_state,
1186 self.server_id,
1187 Some(self.connection_id),
1188 event,
1189 &*self.protocol,
1190 )
1191 .await
1192 {
1193 Ok(result) => result,
1194 Err(e) => {
1195 let failure = WireFailure::classify(&e);
1198 error!(
1199 "M3UA {} decision=fail_closed_llm_error category={:?} peer={}: {:#}",
1200 event.event_type.id, failure, self.remote_addr, e
1201 );
1202 return HandlerOutcome::Failed(failure);
1203 }
1204 };
1205
1206 let mut sent = Vec::new();
1207 let mut closed = false;
1208 let mut queue: Vec<ActionResult> = result.protocol_results;
1209 queue.reverse();
1210 while let Some(item) = queue.pop() {
1211 match item {
1212 ActionResult::Multiple(inner) => {
1213 for nested in inner.into_iter().rev() {
1214 queue.push(nested);
1215 }
1216 }
1217 ActionResult::Output(bytes) => {
1218 if let Some(kind) = codec::peek_class_type(&bytes) {
1221 sent.push(kind);
1222 }
1223 self.send(bytes).await;
1224 }
1225 ActionResult::CloseConnection => {
1226 let mut writer = self.writer.lock().await;
1227 let _ = writer.shutdown().await;
1228 drop(writer);
1229 closed = true;
1230 }
1231 ActionResult::Custom { name, data } if name == ss7::SS7_INTENT => {
1232 let route = self.engine.last_route.clone();
1233 match self.engine.apply(
1234 &data,
1235 route.as_ref(),
1236 0,
1237 crate::utils::clock::Instant::now(),
1238 ) {
1239 Ok((_, out)) => {
1240 sent.push((codec::CLASS_TRANSFER, codec::TRANSFER_DATA));
1241 self.send_ss7(out).await;
1242 }
1243 Err(e) => {
1244 Log::new(Some(&self.status_tx)).warn(format!(
1248 "M3UA {} refused for {}: {} (decision=action_refused)",
1249 data["type"].as_str().unwrap_or("SS7 action"),
1250 self.remote_addr,
1251 e
1252 ));
1253 }
1254 }
1255 }
1256 ActionResult::WaitForMore | ActionResult::NoAction => {}
1257 other => debug!("M3UA ignoring action result {other:?}"),
1258 }
1259 }
1260
1261 HandlerOutcome::Ran { sent, closed }
1262 }
1263
1264 async fn send_ss7(&self, out: ss7::Outgoing) {
1266 trace!(
1267 "M3UA SS7 {} to {} opc={} dpc={}",
1268 out.label,
1269 self.remote_addr,
1270 out.protocol_data.opc,
1271 out.protocol_data.dpc
1272 );
1273 self.send_encoded(
1274 &out.label,
1275 codec::data(
1276 &out.protocol_data,
1277 out.network_appearance,
1278 out.routing_context,
1279 None,
1280 ),
1281 )
1282 .await;
1283 }
1284
1285 async fn expire_dialogues(&mut self, now: crate::utils::clock::Instant) {
1287 for (dialogue, abort) in self.engine.expire(now) {
1288 Log::new(Some(&self.status_tx)).info(format!(
1289 "M3UA dialogue {} with {} idle for {}s; aborted (decision=dialogue_idle_timeout)",
1290 dialogue.id,
1291 self.remote_addr,
1292 ss7::DIALOGUE_IDLE_TIMEOUT.as_secs()
1293 ));
1294 if let Some(out) = abort {
1295 self.send_ss7(out).await;
1296 }
1297 }
1298 }
1299
1300 async fn on_ss7_event(&mut self, ev: ss7::Ss7Event) -> bool {
1304 for reply in ev.replies {
1305 self.send_ss7(reply).await;
1306 }
1307 let event_type: &'static crate::protocol::EventType = match ev.family {
1308 ss7::Family::Map => &ss7_actions::SGP_MAP_EVENT,
1309 ss7::Family::Cap => &ss7_actions::SGP_CAP_EVENT,
1310 };
1311 let mut data = ev.data;
1312 data["connection_id"] = serde_json::json!(self.connection_id.to_string());
1313 data["remote_addr"] = serde_json::json!(self.remote_addr.to_string());
1314 data["transport"] = serde_json::json!(self.config.transport.label());
1315 Log::new(Some(&self.status_tx)).info(format!(
1316 "M3UA {} TC-{} dialogue {} from {}",
1317 ev.family.name().to_uppercase(),
1318 ev.kind.name().to_uppercase(),
1319 ev.dialogue_id,
1320 self.remote_addr
1321 ));
1322 let event = Event { event_type, data };
1323 let outcome = self.call_llm_and_send(&event).await;
1324 let closed = matches!(outcome, HandlerOutcome::Ran { closed: true, .. });
1325 if ev.ended || !self.engine.awaiting_answer(ev.dialogue_id) {
1326 if ev.ended {
1327 self.log_observational_outcome(event_type.id.as_str(), outcome);
1328 }
1329 return !closed;
1330 }
1331 let (fail_closed, decision) = match &outcome {
1335 HandlerOutcome::Ran { .. } => (false, "model_silent".to_string()),
1336 HandlerOutcome::Failed(f) => (
1337 true,
1338 format!("fail_closed_llm_error, {}", failure_category(*f)),
1339 ),
1340 HandlerOutcome::NoPolicy => (true, "no_policy_configured".to_string()),
1341 };
1342 if let Some(out) = self.engine.close_unanswered(ev.dialogue_id, fail_closed) {
1343 Log::new(Some(&self.status_tx)).warn(format!(
1344 "M3UA {} dialogue {} from {} left unanswered (decision={}); sending {}",
1345 ev.family.name().to_uppercase(),
1346 ev.dialogue_id,
1347 self.remote_addr,
1348 decision,
1349 out.label
1350 ));
1351 self.send_ss7(out).await;
1352 }
1353 !closed
1354 }
1355
1356 async fn send(&self, bytes: Vec<u8>) {
1357 let written = {
1358 let mut writer = self.writer.lock().await;
1359 match writer.write_all(&bytes).await {
1360 Ok(()) => writer.flush().await.is_ok(),
1361 Err(_) => false,
1362 }
1363 };
1364 if !written {
1365 debug!(
1366 "M3UA write failed on association {}; peer has gone",
1367 self.connection_id
1368 );
1369 return;
1370 }
1371 self.app_state
1372 .update_connection_stats(
1373 self.server_id,
1374 self.connection_id,
1375 None,
1376 Some(bytes.len() as u64),
1377 None,
1378 Some(1),
1379 )
1380 .await;
1381 }
1382
1383 async fn send_encoded(&self, what: &str, encoded: Result<Vec<u8>, codec::WireError>) {
1391 match encoded {
1392 Ok(bytes) => self.send(bytes).await,
1393 Err(e) => {
1394 error!(
1395 "M3UA decision=fail_closed_encode: refusing to send {} to {}: {}",
1396 what, self.remote_addr, e
1397 );
1398 Log::new(Some(&self.status_tx)).warn(format!(
1399 "M3UA could not encode {what} for {}",
1400 self.remote_addr
1401 ));
1402 }
1403 }
1404 }
1405
1406 async fn send_error(&self, error_code: u32) {
1407 Log::new(Some(&self.status_tx)).warn(format!(
1408 "M3UA ERR 0x{:02x} ({}) to {}",
1409 error_code,
1410 codec::error_code_name(error_code),
1411 self.remote_addr
1412 ));
1413 self.send_encoded("ERR", codec::error(error_code, self.config.routing_context))
1414 .await;
1415 }
1416
1417 async fn set_connection_state(&self) {
1418 let server_id = self.server_id;
1419 let connection_id = self.connection_id;
1420 let value = serde_json::json!({
1421 "asp_state": self.state.as_str(),
1422 "transport": self.config.transport.label(),
1423 });
1424 self.app_state
1425 .with_server_mut(server_id, |server| {
1426 if let Some(connection) = server.connections.get_mut(&connection_id) {
1427 connection.protocol_info =
1428 crate::state::server::ProtocolConnectionInfo::new(value.clone());
1429 }
1430 })
1431 .await;
1432 }
1433}
1434
1435#[derive(Debug, Clone)]
1437enum HandlerOutcome {
1438 NoPolicy,
1441 Failed(WireFailure),
1445 Ran { sent: Vec<(u8, u8)>, closed: bool },
1448}
1449
1450fn failure_category(failure: WireFailure) -> &'static str {
1451 if failure.is_overloaded() {
1456 "resource"
1457 } else {
1458 "unavailable"
1459 }
1460}
1461
1462fn traffic_mode_name(mode: u32) -> &'static str {
1463 match mode {
1464 codec::TRAFFIC_MODE_OVERRIDE => "override",
1465 codec::TRAFFIC_MODE_LOADSHARE => "loadshare",
1466 codec::TRAFFIC_MODE_BROADCAST => "broadcast",
1467 _ => "unassigned",
1468 }
1469}
1470
1471fn render_payload(payload: &[u8]) -> (String, &'static str) {
1478 let printable = std::str::from_utf8(payload).ok().filter(|s| {
1479 s.chars()
1480 .all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t')
1481 });
1482 match printable {
1483 Some(text) if !payload.is_empty() => (text.to_string(), "utf8"),
1484 _ => (hex::encode(payload), "hex"),
1485 }
1486}
1487
1488async fn operator_wants_dynamic(state: &AppState, server_id: ServerId, event_id: &str) -> bool {
1495 state
1496 .with_server_mut(server_id, |server| {
1497 let has_instruction = !server.instruction.trim().is_empty();
1498 let has_handler = server
1499 .event_handler_config
1500 .as_ref()
1501 .map(|c| c.find_handler(event_id).is_some())
1502 .unwrap_or(false);
1503 has_instruction || has_handler
1504 })
1505 .await
1506 .unwrap_or(false)
1507}
1508
1509pub async fn read_message<R: AsyncRead + Unpin>(
1516 reader: &mut R,
1517 read_timeout: std::time::Duration,
1518) -> (Incoming, usize) {
1519 let mut header = [0u8; codec::HEADER_LEN];
1520 let header_read = match tokio::time::timeout(read_timeout, reader.read_exact(&mut header)).await
1521 {
1522 Ok(read) => read,
1523 Err(_) => {
1524 debug!(
1525 "M3UA peer sent nothing for {}s; closing idle association",
1526 read_timeout.as_secs()
1527 );
1528 return (Incoming::Eof, 0);
1529 }
1530 };
1531 match header_read {
1532 Ok(_) => {}
1533 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return (Incoming::Eof, 0),
1534 Err(e) => {
1535 debug!("M3UA read error: {e}");
1536 return (Incoming::Eof, 0);
1537 }
1538 }
1539
1540 let parsed = match codec::parse_header(&header) {
1541 Ok(h) => h,
1542 Err(e) => return (Incoming::HeaderError(e), codec::HEADER_LEN),
1543 };
1544
1545 let total = parsed.length as usize;
1546 let mut full = vec![0u8; total];
1547 full[..codec::HEADER_LEN].copy_from_slice(&header);
1548 if total > codec::HEADER_LEN {
1549 match tokio::time::timeout(
1550 read_timeout,
1551 reader.read_exact(&mut full[codec::HEADER_LEN..]),
1552 )
1553 .await
1554 {
1555 Ok(Ok(_)) => {}
1556 Ok(Err(e)) => {
1557 debug!("M3UA truncated message body: {e}");
1558 return (Incoming::Eof, codec::HEADER_LEN);
1559 }
1560 Err(_) => {
1561 debug!(
1562 "M3UA peer announced {total} octets and stalled for {}s without \
1563 delivering them; closing",
1564 read_timeout.as_secs()
1565 );
1566 return (Incoming::Eof, codec::HEADER_LEN);
1567 }
1568 }
1569 }
1570
1571 let slack = codec::alignment_slack(parsed.length);
1575 if slack > 0 {
1576 let mut padding = [0u8; 3];
1577 match tokio::time::timeout(read_timeout, reader.read_exact(&mut padding[..slack])).await {
1578 Ok(Ok(_)) => {}
1579 _ => return (Incoming::Eof, total),
1580 }
1581 }
1582
1583 let consumed = total + slack;
1584 match codec::Message::parse(&full) {
1585 Ok(message) => (Incoming::Message(message), consumed),
1586 Err(e) => (Incoming::BodyError(e), consumed),
1587 }
1588}