pub mod actions;
pub mod ber;
pub mod cap;
pub mod codec;
pub mod gsm;
pub mod map;
pub mod sccp;
pub mod ss7;
pub mod ss7_actions;
pub mod tcap;
pub mod transport;
use anyhow::{anyhow, Result};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, error, info, trace, warn};
use transport::{Reader, Writer};
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::server::M3uaProtocol;
use crate::state::app_state::AppState;
use crate::state::ServerId;
use crate::utils::WireFailure;
use actions::{
M3UA_ASP_ACTIVE_EVENT, M3UA_ASP_DOWN_EVENT, M3UA_ASP_UP_EVENT, M3UA_DATA_EVENT,
M3UA_ERROR_EVENT,
};
const IPPROTO_SCTP: i32 = 132;
const FIRST_MESSAGE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_MESSAGES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = &[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, ];
pub struct M3uaServer;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum M3uaTransport {
Sctp,
Tcp,
}
impl M3uaTransport {
pub fn parse(value: &str) -> Result<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"sctp" => Ok(Self::Sctp),
"tcp" => Ok(Self::Tcp),
other => Err(anyhow!(
"transport must be \"sctp\" (the transport RFC 4666 defines) or \"tcp\" \
(non-standard, for local protocol work only), got {other:?}"
)),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Sctp => "sctp",
Self::Tcp => "tcp",
}
}
pub fn label(self) -> &'static str {
match self {
Self::Sctp => "sctp (RFC 4666)",
Self::Tcp => "tcp (NON-STANDARD lab transport, not SIGTRAN)",
}
}
}
pub async fn bind_listener(addr: SocketAddr, transport: M3uaTransport) -> Result<TcpListener> {
match transport {
M3uaTransport::Tcp => {
crate::server::socket_helpers::create_reusable_tcp_listener(addr).await
}
M3uaTransport::Sctp => bind_sctp_listener(addr),
}
}
fn bind_sctp_listener(addr: SocketAddr) -> Result<TcpListener> {
use socket2::{Domain, Protocol, Socket, Type};
let domain = if addr.is_ipv4() {
Domain::IPV4
} else {
Domain::IPV6
};
let socket =
Socket::new(domain, Type::STREAM, Some(Protocol::from(IPPROTO_SCTP))).map_err(|e| {
anyhow!(
"M3UA cannot open an SCTP socket on this host: {e}. M3UA's transport is SCTP \
(RFC 4666 section 1.4.1, IANA port 2905) and this operating system has no SCTP \
stack available to this process — macOS ships neither kernel support nor \
headers for SCTP, and a Linux kernel without the sctp module fails the same \
way. NetGet refuses to start rather than pretend it is speaking SIGTRAN. \
Either run this server on a host with SCTP, or, for local protocol work only, \
pass the startup parameter transport=\"tcp\" — a NON-STANDARD framing that \
exercises the M3UA layer and that no real SIGTRAN peer speaks."
)
})?;
socket
.set_reuse_address(true)
.map_err(|e| anyhow!("M3UA could not set SO_REUSEADDR on the SCTP socket: {e}"))?;
socket
.bind(&addr.into())
.map_err(|e| anyhow!("M3UA could not bind the SCTP socket to {addr}: {e}"))?;
socket
.listen(128)
.map_err(|e| anyhow!("M3UA could not listen on the SCTP socket at {addr}: {e}"))?;
socket
.set_nonblocking(true)
.map_err(|e| anyhow!("M3UA could not put the SCTP socket in non-blocking mode: {e}"))?;
let std_listener: std::net::TcpListener = socket.into();
TcpListener::from_std(std_listener)
.map_err(|e| anyhow!("M3UA could not register the SCTP listener with tokio: {e}"))
}
#[derive(Clone, Copy, Debug)]
pub struct M3uaConfig {
pub transport: M3uaTransport,
pub routing_context: Option<u32>,
pub network_appearance: Option<u32>,
}
impl M3uaConfig {
fn from_params(params: Option<&crate::protocol::StartupParams>) -> Result<Self> {
let Some(params) = params else {
return Ok(Self {
transport: M3uaTransport::Sctp,
routing_context: None,
network_appearance: None,
});
};
let transport = match params.get_optional_string("transport")? {
Some(s) => M3uaTransport::parse(&s)?,
None => M3uaTransport::Sctp,
};
Ok(Self {
transport,
routing_context: params.get_optional_u32("routing_context")?,
network_appearance: params.get_optional_u32("network_appearance")?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AspState {
Down,
Inactive,
Active,
}
impl AspState {
fn as_str(self) -> &'static str {
match self {
Self::Down => "ASP-DOWN",
Self::Inactive => "ASP-INACTIVE",
Self::Active => "ASP-ACTIVE",
}
}
}
impl M3uaServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: ServerId,
startup_params: Option<crate::protocol::StartupParams>,
) -> Result<SocketAddr> {
let config = M3uaConfig::from_params(startup_params.as_ref())?;
let listener = bind_listener(listen_addr, config.transport).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"M3UA SGP listening on {} over {}",
local_addr,
config.transport.label()
));
if config.transport == M3uaTransport::Tcp {
Log::new(Some(&status_tx)).warn(
"M3UA is running over TCP. This is NOT SIGTRAN: RFC 4666 defines M3UA over \
SCTP only, and no real ASP will connect over TCP. Use it for local protocol \
work and nothing else.",
);
}
let protocol = Arc::new(M3uaProtocol::new());
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"M3UA",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
Log::new(Some(&status_tx)).info(format!(
"M3UA association {} from {} ({})",
connection_id,
remote_addr,
config.transport.as_str()
));
register_connection(
&app_state,
server_id,
connection_id,
remote_addr,
config.transport,
)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = run_session(
stream,
connection_id,
server_id,
remote_addr,
llm_clone,
state_clone.clone(),
status_clone.clone(),
protocol_clone,
config,
)
.await
{
Log::new(Some(&status_clone))
.error(format!("M3UA association error: {}", e));
}
state_clone
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
state_clone
.close_connection_on_server(server_id, connection_id)
.await;
Log::new(Some(&status_clone))
.info(format!("M3UA association {} closed", connection_id));
let _ = status_clone.send("__UPDATE_UI__".to_string());
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept M3UA association: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
async fn register_connection(
app_state: &AppState,
server_id: ServerId,
connection_id: ConnectionId,
remote_addr: SocketAddr,
transport: M3uaTransport,
) {
use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
let now = crate::utils::clock::Instant::now();
app_state
.add_connection_to_server(
server_id,
ConnectionState {
id: connection_id,
remote_addr,
local_addr: remote_addr,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
"asp_state": AspState::Down.as_str(),
"transport": transport.label(),
})),
},
)
.await;
}
struct M3uaSession {
connection_id: ConnectionId,
server_id: ServerId,
remote_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<M3uaProtocol>,
config: M3uaConfig,
state: AspState,
asp_identifier: Option<u32>,
writer: Arc<Mutex<Writer>>,
engine: ss7::Engine,
}
pub enum Incoming {
Message(codec::Message),
HeaderError(codec::WireError),
BodyError(codec::WireError),
Eof,
}
#[allow(clippy::too_many_arguments)]
async fn run_session(
stream: TcpStream,
connection_id: ConnectionId,
server_id: ServerId,
remote_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<M3uaProtocol>,
config: M3uaConfig,
) -> Result<()> {
let (mut reader, writer) = transport::split(stream, config.transport == M3uaTransport::Sctp);
let writer = Arc::new(Mutex::new(writer));
let peer_rx = crate::server::peer_support::register_peer_channel(
&app_state,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
Arc::new(M3uaProtocol::for_peer_commands()),
app_state.clone(),
server_id,
connection_id.as_u32(),
writer.clone(),
status_tx.clone(),
);
let mut session = M3uaSession {
connection_id,
server_id,
remote_addr,
llm_client,
app_state,
status_tx,
protocol,
config,
state: AspState::Down,
asp_identifier: None,
writer,
engine: ss7::Engine::new(),
};
session.run(&mut reader).await
}
impl M3uaSession {
async fn run(&mut self, reader: &mut Reader) -> Result<()> {
debug!(
"M3UA association {} open, ASP in {}",
self.connection_id,
self.state.as_str()
);
let mut received_one = false;
loop {
let read_timeout = if received_one {
IDLE_BETWEEN_MESSAGES_TIMEOUT
} else {
FIRST_MESSAGE_READ_TIMEOUT
};
let (incoming, bytes_in) = read_message(reader, read_timeout).await;
if !matches!(incoming, Incoming::Eof) {
received_one = true;
}
if bytes_in > 0 {
self.app_state
.update_connection_stats(
self.server_id,
self.connection_id,
Some(bytes_in as u64),
None,
Some(1),
None,
)
.await;
}
match incoming {
Incoming::Eof => {
debug!("M3UA association {} closed by peer", self.connection_id);
break;
}
Incoming::HeaderError(e) => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA header rejected from {}: {}",
self.remote_addr, e
));
self.send_error(e.error_code).await;
break;
}
Incoming::BodyError(e) => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA message from {} rejected: {}",
self.remote_addr, e
));
self.send_error(e.error_code).await;
}
Incoming::Message(message) => {
if !self.dispatch(message).await? {
break;
}
}
}
}
Ok(())
}
async fn dispatch(&mut self, message: codec::Message) -> Result<bool> {
trace!(
"M3UA {} from {} in {}",
message.name(),
self.remote_addr,
self.state.as_str()
);
match (message.class, message.msg_type) {
(codec::CLASS_ASPSM, codec::ASPSM_ASPUP) => self.on_asp_up(&message).await,
(codec::CLASS_ASPSM, codec::ASPSM_ASPDN) => self.on_asp_down(&message).await,
(codec::CLASS_ASPSM, codec::ASPSM_BEAT) => {
let heartbeat = message
.param(codec::TAG_HEARTBEAT_DATA)
.map(|p| p.value.clone());
trace!("M3UA BEAT from {} -> BEAT ACK", self.remote_addr);
match codec::beat_ack(heartbeat.as_deref()) {
Ok(bytes) => self.send(bytes).await,
Err(e) => {
error!(
"M3UA decision=fail_closed_encode: BEAT from {} carries Heartbeat \
Data that cannot be echoed within the message ceiling: {}",
self.remote_addr, e
);
self.send_error(codec::ERR_PROTOCOL_ERROR).await;
}
}
Ok(true)
}
(codec::CLASS_ASPTM, codec::ASPTM_ASPAC) => self.on_asp_active(&message).await,
(codec::CLASS_ASPTM, codec::ASPTM_ASPIA) => {
self.state = AspState::Inactive;
self.set_connection_state().await;
Log::new(Some(&self.status_tx)).info(format!(
"M3UA ASPIA from {}, ASP is now {}",
self.remote_addr,
self.state.as_str()
));
self.send_encoded(
"ASPIA ACK",
codec::aspia_ack(message.param_u32(codec::TAG_ROUTING_CONTEXT)),
)
.await;
Ok(true)
}
(codec::CLASS_TRANSFER, codec::TRANSFER_DATA) => self.on_data(&message).await,
(codec::CLASS_MGMT, codec::MGMT_ERR) => self.on_error_received(&message).await,
(codec::CLASS_MGMT, codec::MGMT_NTFY) => {
Log::new(Some(&self.status_tx)).info(format!(
"M3UA NTFY from {} status=0x{:08x}",
self.remote_addr,
message.param_u32(codec::TAG_STATUS).unwrap_or(0)
));
Ok(true)
}
(codec::CLASS_RKM, _) => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA {} from {} refused: NetGet implements no routing key table \
(dynamic registration is storage; configure routing_context instead)",
message.name(),
self.remote_addr
));
self.send_error(codec::ERR_UNSUPPORTED_MESSAGE_TYPE).await;
Ok(true)
}
(codec::CLASS_SSNM, _) => {
warn!(
"M3UA {} from {} is an SGP-to-ASP message; refusing",
message.name(),
self.remote_addr
);
self.send_error(codec::ERR_UNEXPECTED_MESSAGE).await;
Ok(true)
}
(class, msg_type) => {
let known_class = matches!(
class,
codec::CLASS_MGMT
| codec::CLASS_TRANSFER
| codec::CLASS_SSNM
| codec::CLASS_ASPSM
| codec::CLASS_ASPTM
| codec::CLASS_RKM
);
let code = if known_class {
codec::ERR_UNSUPPORTED_MESSAGE_TYPE
} else {
codec::ERR_UNSUPPORTED_MESSAGE_CLASS
};
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA class {} ({}) type {} from {} is not supported",
class,
codec::class_name(class),
msg_type,
self.remote_addr
));
self.send_error(code).await;
Ok(true)
}
}
}
async fn on_asp_up(&mut self, message: &codec::Message) -> Result<bool> {
self.asp_identifier = message.param_u32(codec::TAG_ASP_IDENTIFIER);
if self.state != AspState::Down {
debug!(
"M3UA duplicate ASPUP from {} while {}; re-acknowledging",
self.remote_addr,
self.state.as_str()
);
self.send_encoded("ASPUP ACK", codec::aspup_ack(None)).await;
return Ok(true);
}
let info_string = message
.param(codec::TAG_INFO_STRING)
.map(|p| String::from_utf8_lossy(&p.value).to_string());
Log::new(Some(&self.status_tx)).info(format!(
"M3UA ASPUP from {} (asp_id={:?})",
self.remote_addr, self.asp_identifier
));
let event = Event {
event_type: &M3UA_ASP_UP_EVENT,
data: serde_json::json!({
"connection_id": self.connection_id.to_string(),
"remote_addr": self.remote_addr.to_string(),
"transport": self.config.transport.label(),
"asp_identifier": self.asp_identifier,
"info_string": info_string,
"asp_state": self.state.as_str(),
}),
};
self.decide_admission(
&event,
(codec::CLASS_ASPSM, codec::ASPSM_ASPUP_ACK),
AspState::Inactive,
)
.await
}
async fn on_asp_active(&mut self, message: &codec::Message) -> Result<bool> {
if self.state == AspState::Down {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA ASPAC from {} while ASP-DOWN; refusing without consulting policy",
self.remote_addr
));
self.send_error(codec::ERR_UNEXPECTED_MESSAGE).await;
return Ok(true);
}
let routing_context = message.param_u32(codec::TAG_ROUTING_CONTEXT);
if let Some(expected) = self.config.routing_context {
if let Some(offered) = routing_context {
if offered != expected {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA ASPAC from {} names routing context {} but this SGP serves {}",
self.remote_addr, offered, expected
));
self.send_error(codec::ERR_INVALID_ROUTING_CONTEXT).await;
return Ok(true);
}
}
}
if self.state == AspState::Active {
debug!(
"M3UA duplicate ASPAC from {} while ASP-ACTIVE; re-acknowledging",
self.remote_addr
);
self.send_encoded(
"ASPAC ACK",
codec::aspac_ack(
message.param_u32(codec::TAG_TRAFFIC_MODE_TYPE),
routing_context,
None,
),
)
.await;
return Ok(true);
}
let traffic_mode = message.param_u32(codec::TAG_TRAFFIC_MODE_TYPE);
Log::new(Some(&self.status_tx)).info(format!(
"M3UA ASPAC from {} (rc={:?}, traffic_mode={:?})",
self.remote_addr, routing_context, traffic_mode
));
let event = Event {
event_type: &M3UA_ASP_ACTIVE_EVENT,
data: serde_json::json!({
"connection_id": self.connection_id.to_string(),
"remote_addr": self.remote_addr.to_string(),
"transport": self.config.transport.label(),
"asp_identifier": self.asp_identifier,
"routing_context": routing_context,
"traffic_mode": traffic_mode,
"traffic_mode_name": traffic_mode.map(traffic_mode_name),
"asp_state": self.state.as_str(),
}),
};
self.decide_admission(
&event,
(codec::CLASS_ASPTM, codec::ASPTM_ASPAC_ACK),
AspState::Active,
)
.await
}
async fn decide_admission(
&mut self,
event: &Event,
ack: (u8, u8),
next_state: AspState,
) -> Result<bool> {
let event_id = event.event_type.id.clone();
let outcome = self.call_llm_and_send(event).await;
match outcome {
HandlerOutcome::Ran { ref sent, closed } if sent.contains(&ack) => {
self.state = next_state;
self.set_connection_state().await;
Log::new(Some(&self.status_tx)).info(format!(
"M3UA {} admitted {} to {}",
event_id,
self.remote_addr,
self.state.as_str()
));
Ok(!closed)
}
HandlerOutcome::Ran { ref sent, closed }
if sent.contains(&(codec::CLASS_MGMT, codec::MGMT_ERR)) =>
{
info!(
"M3UA {} decision=model_reject peer={} asp stays {}",
event_id,
self.remote_addr,
self.state.as_str()
);
Ok(!closed)
}
HandlerOutcome::Ran { closed, .. } => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA {} produced no acknowledgement for {} (decision=model_silent); \
refusing, ASP stays {}",
event_id,
self.remote_addr,
self.state.as_str()
));
self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
.await;
Ok(!closed)
}
HandlerOutcome::Failed(failure) => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA {} could not be evaluated for {} (decision=fail_closed_llm_error, \
{}); refusing, ASP stays {}",
event_id,
self.remote_addr,
failure_category(failure),
self.state.as_str()
));
self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
.await;
Ok(true)
}
HandlerOutcome::NoPolicy => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA {} refused for {} (decision=no_policy_configured): this server has \
no instruction and no event handler, so nothing decides which ASPs may \
join. Give the server an instruction, or a handler for {}.",
event_id, self.remote_addr, event_id
));
self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
.await;
Ok(true)
}
}
}
async fn on_asp_down(&mut self, _message: &codec::Message) -> Result<bool> {
self.send_encoded("ASPDN ACK", codec::aspdn_ack()).await;
self.state = AspState::Down;
self.set_connection_state().await;
Log::new(Some(&self.status_tx)).info(format!(
"M3UA ASPDN from {}, ASP is now {}",
self.remote_addr,
self.state.as_str()
));
let event = Event {
event_type: &M3UA_ASP_DOWN_EVENT,
data: serde_json::json!({
"connection_id": self.connection_id.to_string(),
"remote_addr": self.remote_addr.to_string(),
"transport": self.config.transport.label(),
"asp_identifier": self.asp_identifier,
"asp_state": self.state.as_str(),
}),
};
let outcome = self.call_llm_and_send(&event).await;
self.log_observational_outcome("m3ua_asp_down_received", outcome);
Ok(true)
}
async fn on_error_received(&mut self, message: &codec::Message) -> Result<bool> {
let code = message.param_u32(codec::TAG_ERROR_CODE).unwrap_or(0);
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA ERR from {}: 0x{:02x} {}",
self.remote_addr,
code,
codec::error_code_name(code)
));
let event = Event {
event_type: &M3UA_ERROR_EVENT,
data: serde_json::json!({
"connection_id": self.connection_id.to_string(),
"remote_addr": self.remote_addr.to_string(),
"transport": self.config.transport.label(),
"error_code": code,
"error_name": codec::error_code_name(code),
"routing_context": message.param_u32(codec::TAG_ROUTING_CONTEXT),
"asp_state": self.state.as_str(),
}),
};
let outcome = self.call_llm_and_send(&event).await;
self.log_observational_outcome("m3ua_error_received", outcome);
Ok(true)
}
async fn on_data(&mut self, message: &codec::Message) -> Result<bool> {
if self.state != AspState::Active {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA DATA from {} while {}; refusing",
self.remote_addr,
self.state.as_str()
));
self.send_error(codec::ERR_UNEXPECTED_MESSAGE).await;
return Ok(true);
}
let routing_context = message.param_u32(codec::TAG_ROUTING_CONTEXT);
if let (Some(expected), Some(offered)) = (self.config.routing_context, routing_context) {
if expected != offered {
self.send_error(codec::ERR_INVALID_ROUTING_CONTEXT).await;
return Ok(true);
}
}
let network_appearance = message.param_u32(codec::TAG_NETWORK_APPEARANCE);
if let (Some(expected), Some(offered)) =
(self.config.network_appearance, network_appearance)
{
if expected != offered {
self.send_error(codec::ERR_INVALID_NETWORK_APPEARANCE).await;
return Ok(true);
}
}
let Some(parameter) = message.param(codec::TAG_PROTOCOL_DATA) else {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA DATA from {} carries no Protocol Data parameter",
self.remote_addr
));
self.send_error(codec::ERR_MISSING_PARAMETER).await;
return Ok(true);
};
let protocol_data = match codec::ProtocolData::parse(¶meter.value) {
Ok(pd) => pd,
Err(e) => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA DATA from {} has malformed Protocol Data: {}",
self.remote_addr, e
));
self.send_error(e.error_code).await;
return Ok(true);
}
};
let now = crate::utils::clock::Instant::now();
self.expire_dialogues(now).await;
let mut sccp_note = None;
match self
.engine
.on_data(&protocol_data, routing_context, network_appearance, now)
{
ss7::Inbound::NotSccp => {}
ss7::Inbound::Undecoded(reason) => sccp_note = Some(reason),
ss7::Inbound::Handled { replies, note } => {
Log::new(Some(&self.status_tx)).info(format!(
"M3UA SCCP from {} opc={} handled without the model: {}",
self.remote_addr, protocol_data.opc, note
));
for reply in replies {
self.send_ss7(reply).await;
}
return Ok(true);
}
ss7::Inbound::Event(event) => return Ok(self.on_ss7_event(event).await),
}
let (payload, encoding) = render_payload(&protocol_data.payload);
info!(
"M3UA DATA from {} opc={} dpc={} si={} ({}) sls={}",
self.remote_addr,
protocol_data.opc,
protocol_data.dpc,
protocol_data.si,
codec::si_name(protocol_data.si),
protocol_data.sls
);
let event = Event {
event_type: &M3UA_DATA_EVENT,
data: serde_json::json!({
"connection_id": self.connection_id.to_string(),
"remote_addr": self.remote_addr.to_string(),
"transport": self.config.transport.label(),
"opc": protocol_data.opc,
"dpc": protocol_data.dpc,
"si": protocol_data.si,
"si_name": codec::si_name(protocol_data.si),
"ni": protocol_data.ni,
"mp": protocol_data.mp,
"sls": protocol_data.sls,
"payload": payload,
"encoding": encoding,
"payload_bytes": protocol_data.payload.len(),
"routing_context": routing_context,
"network_appearance": network_appearance,
"correlation_id": message.param_u32(codec::TAG_CORRELATION_ID),
"sccp_note": sccp_note,
}),
};
let outcome = self.call_llm_and_send(&event).await;
match outcome {
HandlerOutcome::Ran { closed, .. } => Ok(!closed),
HandlerOutcome::Failed(failure) => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA m3ua_data_received could not be evaluated for {} \
(decision=fail_closed_llm_error, {}); answering ERR",
self.remote_addr,
failure_category(failure)
));
self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
.await;
Ok(true)
}
HandlerOutcome::NoPolicy => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA m3ua_data_received refused for {} (decision=no_policy_configured)",
self.remote_addr
));
self.send_error(codec::ERR_REFUSED_MANAGEMENT_BLOCKING)
.await;
Ok(true)
}
}
}
fn log_observational_outcome(&self, event_id: &str, outcome: HandlerOutcome) {
match outcome {
HandlerOutcome::Failed(failure) => Log::new(Some(&self.status_tx)).warn(format!(
"M3UA {} could not be evaluated for {} (decision=fail_closed_llm_error, {}); \
nothing sent",
event_id,
self.remote_addr,
failure_category(failure)
)),
HandlerOutcome::NoPolicy => debug!(
"M3UA {} peer={}: no operator policy configured, no LLM call",
event_id, self.remote_addr
),
HandlerOutcome::Ran { ref sent, .. } if sent.is_empty() => debug!(
"M3UA {} decision=model_silent peer={}: nothing sent",
event_id, self.remote_addr
),
HandlerOutcome::Ran { .. } => debug!(
"M3UA {} peer={}: handler answered",
event_id, self.remote_addr
),
}
}
async fn call_llm_and_send(&mut self, event: &Event) -> HandlerOutcome {
if !operator_wants_dynamic(&self.app_state, self.server_id, &event.event_type.id).await {
debug!(
"M3UA {}: no operator policy configured, no LLM call",
event.event_type.id
);
return HandlerOutcome::NoPolicy;
}
let result = match call_llm(
&self.llm_client,
&self.app_state,
self.server_id,
Some(self.connection_id),
event,
&*self.protocol,
)
.await
{
Ok(result) => result,
Err(e) => {
let failure = WireFailure::classify(&e);
error!(
"M3UA {} decision=fail_closed_llm_error category={:?} peer={}: {:#}",
event.event_type.id, failure, self.remote_addr, e
);
return HandlerOutcome::Failed(failure);
}
};
let mut sent = Vec::new();
let mut closed = false;
let mut queue: Vec<ActionResult> = result.protocol_results;
queue.reverse();
while let Some(item) = queue.pop() {
match item {
ActionResult::Multiple(inner) => {
for nested in inner.into_iter().rev() {
queue.push(nested);
}
}
ActionResult::Output(bytes) => {
if let Some(kind) = codec::peek_class_type(&bytes) {
sent.push(kind);
}
self.send(bytes).await;
}
ActionResult::CloseConnection => {
let mut writer = self.writer.lock().await;
let _ = writer.shutdown().await;
drop(writer);
closed = true;
}
ActionResult::Custom { name, data } if name == ss7::SS7_INTENT => {
let route = self.engine.last_route.clone();
match self.engine.apply(
&data,
route.as_ref(),
0,
crate::utils::clock::Instant::now(),
) {
Ok((_, out)) => {
sent.push((codec::CLASS_TRANSFER, codec::TRANSFER_DATA));
self.send_ss7(out).await;
}
Err(e) => {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA {} refused for {}: {} (decision=action_refused)",
data["type"].as_str().unwrap_or("SS7 action"),
self.remote_addr,
e
));
}
}
}
ActionResult::WaitForMore | ActionResult::NoAction => {}
other => debug!("M3UA ignoring action result {other:?}"),
}
}
HandlerOutcome::Ran { sent, closed }
}
async fn send_ss7(&self, out: ss7::Outgoing) {
trace!(
"M3UA SS7 {} to {} opc={} dpc={}",
out.label,
self.remote_addr,
out.protocol_data.opc,
out.protocol_data.dpc
);
self.send_encoded(
&out.label,
codec::data(
&out.protocol_data,
out.network_appearance,
out.routing_context,
None,
),
)
.await;
}
async fn expire_dialogues(&mut self, now: crate::utils::clock::Instant) {
for (dialogue, abort) in self.engine.expire(now) {
Log::new(Some(&self.status_tx)).info(format!(
"M3UA dialogue {} with {} idle for {}s; aborted (decision=dialogue_idle_timeout)",
dialogue.id,
self.remote_addr,
ss7::DIALOGUE_IDLE_TIMEOUT.as_secs()
));
if let Some(out) = abort {
self.send_ss7(out).await;
}
}
}
async fn on_ss7_event(&mut self, ev: ss7::Ss7Event) -> bool {
for reply in ev.replies {
self.send_ss7(reply).await;
}
let event_type: &'static crate::protocol::EventType = match ev.family {
ss7::Family::Map => &ss7_actions::SGP_MAP_EVENT,
ss7::Family::Cap => &ss7_actions::SGP_CAP_EVENT,
};
let mut data = ev.data;
data["connection_id"] = serde_json::json!(self.connection_id.to_string());
data["remote_addr"] = serde_json::json!(self.remote_addr.to_string());
data["transport"] = serde_json::json!(self.config.transport.label());
Log::new(Some(&self.status_tx)).info(format!(
"M3UA {} TC-{} dialogue {} from {}",
ev.family.name().to_uppercase(),
ev.kind.name().to_uppercase(),
ev.dialogue_id,
self.remote_addr
));
let event = Event { event_type, data };
let outcome = self.call_llm_and_send(&event).await;
let closed = matches!(outcome, HandlerOutcome::Ran { closed: true, .. });
if ev.ended || !self.engine.awaiting_answer(ev.dialogue_id) {
if ev.ended {
self.log_observational_outcome(event_type.id.as_str(), outcome);
}
return !closed;
}
let (fail_closed, decision) = match &outcome {
HandlerOutcome::Ran { .. } => (false, "model_silent".to_string()),
HandlerOutcome::Failed(f) => (
true,
format!("fail_closed_llm_error, {}", failure_category(*f)),
),
HandlerOutcome::NoPolicy => (true, "no_policy_configured".to_string()),
};
if let Some(out) = self.engine.close_unanswered(ev.dialogue_id, fail_closed) {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA {} dialogue {} from {} left unanswered (decision={}); sending {}",
ev.family.name().to_uppercase(),
ev.dialogue_id,
self.remote_addr,
decision,
out.label
));
self.send_ss7(out).await;
}
!closed
}
async fn send(&self, bytes: Vec<u8>) {
let written = {
let mut writer = self.writer.lock().await;
match writer.write_all(&bytes).await {
Ok(()) => writer.flush().await.is_ok(),
Err(_) => false,
}
};
if !written {
debug!(
"M3UA write failed on association {}; peer has gone",
self.connection_id
);
return;
}
self.app_state
.update_connection_stats(
self.server_id,
self.connection_id,
None,
Some(bytes.len() as u64),
None,
Some(1),
)
.await;
}
async fn send_encoded(&self, what: &str, encoded: Result<Vec<u8>, codec::WireError>) {
match encoded {
Ok(bytes) => self.send(bytes).await,
Err(e) => {
error!(
"M3UA decision=fail_closed_encode: refusing to send {} to {}: {}",
what, self.remote_addr, e
);
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA could not encode {what} for {}",
self.remote_addr
));
}
}
}
async fn send_error(&self, error_code: u32) {
Log::new(Some(&self.status_tx)).warn(format!(
"M3UA ERR 0x{:02x} ({}) to {}",
error_code,
codec::error_code_name(error_code),
self.remote_addr
));
self.send_encoded("ERR", codec::error(error_code, self.config.routing_context))
.await;
}
async fn set_connection_state(&self) {
let server_id = self.server_id;
let connection_id = self.connection_id;
let value = serde_json::json!({
"asp_state": self.state.as_str(),
"transport": self.config.transport.label(),
});
self.app_state
.with_server_mut(server_id, |server| {
if let Some(connection) = server.connections.get_mut(&connection_id) {
connection.protocol_info =
crate::state::server::ProtocolConnectionInfo::new(value.clone());
}
})
.await;
}
}
#[derive(Debug, Clone)]
enum HandlerOutcome {
NoPolicy,
Failed(WireFailure),
Ran { sent: Vec<(u8, u8)>, closed: bool },
}
fn failure_category(failure: WireFailure) -> &'static str {
if failure.is_overloaded() {
"resource"
} else {
"unavailable"
}
}
fn traffic_mode_name(mode: u32) -> &'static str {
match mode {
codec::TRAFFIC_MODE_OVERRIDE => "override",
codec::TRAFFIC_MODE_LOADSHARE => "loadshare",
codec::TRAFFIC_MODE_BROADCAST => "broadcast",
_ => "unassigned",
}
}
fn render_payload(payload: &[u8]) -> (String, &'static str) {
let printable = std::str::from_utf8(payload).ok().filter(|s| {
s.chars()
.all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t')
});
match printable {
Some(text) if !payload.is_empty() => (text.to_string(), "utf8"),
_ => (hex::encode(payload), "hex"),
}
}
async fn operator_wants_dynamic(state: &AppState, server_id: ServerId, event_id: &str) -> bool {
state
.with_server_mut(server_id, |server| {
let has_instruction = !server.instruction.trim().is_empty();
let has_handler = server
.event_handler_config
.as_ref()
.map(|c| c.find_handler(event_id).is_some())
.unwrap_or(false);
has_instruction || has_handler
})
.await
.unwrap_or(false)
}
pub async fn read_message<R: AsyncRead + Unpin>(
reader: &mut R,
read_timeout: std::time::Duration,
) -> (Incoming, usize) {
let mut header = [0u8; codec::HEADER_LEN];
let header_read = match tokio::time::timeout(read_timeout, reader.read_exact(&mut header)).await
{
Ok(read) => read,
Err(_) => {
debug!(
"M3UA peer sent nothing for {}s; closing idle association",
read_timeout.as_secs()
);
return (Incoming::Eof, 0);
}
};
match header_read {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return (Incoming::Eof, 0),
Err(e) => {
debug!("M3UA read error: {e}");
return (Incoming::Eof, 0);
}
}
let parsed = match codec::parse_header(&header) {
Ok(h) => h,
Err(e) => return (Incoming::HeaderError(e), codec::HEADER_LEN),
};
let total = parsed.length as usize;
let mut full = vec![0u8; total];
full[..codec::HEADER_LEN].copy_from_slice(&header);
if total > codec::HEADER_LEN {
match tokio::time::timeout(
read_timeout,
reader.read_exact(&mut full[codec::HEADER_LEN..]),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => {
debug!("M3UA truncated message body: {e}");
return (Incoming::Eof, codec::HEADER_LEN);
}
Err(_) => {
debug!(
"M3UA peer announced {total} octets and stalled for {}s without \
delivering them; closing",
read_timeout.as_secs()
);
return (Incoming::Eof, codec::HEADER_LEN);
}
}
}
let slack = codec::alignment_slack(parsed.length);
if slack > 0 {
let mut padding = [0u8; 3];
match tokio::time::timeout(read_timeout, reader.read_exact(&mut padding[..slack])).await {
Ok(Ok(_)) => {}
_ => return (Incoming::Eof, total),
}
}
let consumed = total + slack;
match codec::Message::parse(&full) {
Ok(message) => (Incoming::Message(message), consumed),
Err(e) => (Incoming::BodyError(e), consumed),
}
}