pub mod actions;
pub mod v5;
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::state::app_state::AppState;
use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
use actions::{
MqttProtocol, MQTT_CONNECT_EVENT, MQTT_PUBLISH_EVENT, MQTT_SUBSCRIBE_EVENT,
MQTT_UNSUBSCRIBE_EVENT,
};
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
use tracing::{debug, error, info, trace, warn};
const DEFAULT_MAX_PACKET_SIZE: usize = 256 * 1024;
pub const MAX_PACKET_SIZE_LIMIT: usize = 16 * 1024 * 1024;
pub const PKT_CONNECT: u8 = 1;
pub const PKT_CONNACK: u8 = 2;
pub const PKT_PUBLISH: u8 = 3;
pub const PKT_PUBACK: u8 = 4;
pub const PKT_PUBREC: u8 = 5;
pub const PKT_PUBREL: u8 = 6;
pub const PKT_PUBCOMP: u8 = 7;
pub const PKT_SUBSCRIBE: u8 = 8;
pub const PKT_SUBACK: u8 = 9;
pub const PKT_UNSUBSCRIBE: u8 = 10;
pub const PKT_UNSUBACK: u8 = 11;
pub const PKT_PINGREQ: u8 = 12;
pub const PKT_PINGRESP: u8 = 13;
pub const PKT_DISCONNECT: u8 = 14;
pub const PKT_AUTH: u8 = 15;
pub const TOPIC_ALIAS_MAX: u16 = 16;
pub const CONNACK_SERVER_UNAVAILABLE: u8 = 3;
pub const SUBACK_FAILURE: u8 = 0x80;
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = &[PKT_CONNACK << 4, 0x02, 0x00, CONNACK_SERVER_UNAVAILABLE];
pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const IDLE_WITHOUT_KEEP_ALIVE: Duration = Duration::from_secs(900);
#[derive(Clone, Copy, Debug)]
pub struct MqttBounds {
pub connect: Duration,
pub idle_without_keep_alive: Duration,
}
pub fn session_idle_bound(keep_alive_secs: u16, fallback: Duration) -> Duration {
if keep_alive_secs == 0 {
fallback
} else {
Duration::from_millis(u64::from(keep_alive_secs) * 1500)
}
}
pub struct MqttServer;
impl MqttServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
startup_params: Option<crate::protocol::StartupParams>,
) -> Result<SocketAddr> {
let max_packet_size = match startup_params.as_ref() {
Some(params) => match params.get_optional_u64("max_packet_size") {
Ok(Some(v)) => (v as usize).clamp(64, MAX_PACKET_SIZE_LIMIT),
Ok(None) => DEFAULT_MAX_PACKET_SIZE,
Err(e) => return Err(anyhow::anyhow!("MQTT startup parameter error: {}", e)),
},
None => DEFAULT_MAX_PACKET_SIZE,
};
let secs = |name: &str| -> Result<Option<Duration>> {
match startup_params.as_ref() {
Some(params) => params
.get_optional_u64(name)
.map(|v| v.map(Duration::from_secs))
.map_err(|e| anyhow::anyhow!("MQTT startup parameter error: {}", e)),
None => Ok(None),
}
};
let bounds = MqttBounds {
connect: secs("first_byte_timeout_secs")?.unwrap_or(CONNECT_TIMEOUT),
idle_without_keep_alive: secs("idle_timeout_secs")?.unwrap_or(IDLE_WITHOUT_KEEP_ALIVE),
};
let listener = TcpListener::bind(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!("MQTT broker listening on {}", local_addr));
let accept_state = app_state.clone();
let accept_status_tx = status_tx.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,
"MQTT",
Some(&accept_status_tx),
)
.await
{
Ok((socket, peer_addr, permit)) => {
Log::new(Some(&accept_status_tx))
.debug(format!("MQTT connection from {}", peer_addr));
let llm_client = llm_client.clone();
let app_state = accept_state.clone();
let status_tx = accept_status_tx.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let permit = std::sync::Arc::new(permit);
if let Err(e) = handle_mqtt_connection(
socket,
peer_addr,
local_addr,
llm_client,
app_state,
status_tx,
server_id,
max_packet_size,
permit,
bounds,
)
.await
{
error!("MQTT connection error ({}): {}", peer_addr, e);
}
})
.await;
}
Err(e) => {
Log::new(Some(&accept_status_tx))
.error(format!("MQTT accept error: {}", e));
break;
}
}
}
});
app_state
.register_server_task(server_id, accept_handle)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
Ok(local_addr)
}
}
struct RawPacket {
packet_type: u8,
flags: u8,
body: Vec<u8>,
}
#[derive(Default)]
struct Session {
client_id: Option<String>,
keep_alive: Option<u16>,
v5: bool,
aliases: std::collections::HashMap<u16, String>,
}
impl Session {
fn refuse(&self, out_tx: &mpsc::UnboundedSender<Vec<u8>>, reason: u8) {
if self.v5 {
let _ = out_tx.send(build_disconnect_v5(reason, &v5::PropertyWriter::new()));
}
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_mqtt_connection(
socket: TcpStream,
peer_addr: SocketAddr,
local_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
max_packet_size: usize,
permit: Arc<crate::server::accept_bounded::ConnectionPermit>,
bounds: MqttBounds,
) -> Result<()> {
let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
let (mut read_half, write_half) = tokio::io::split(socket);
let write_half = Arc::new(tokio::sync::Mutex::new(write_half));
let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Vec<u8>>();
let writer_status_tx = status_tx.clone();
let writer_write_half = write_half.clone();
let writer_state = app_state.clone();
let writer_permit = Arc::clone(&permit);
let client_max_packet = Arc::new(std::sync::atomic::AtomicU32::new(0));
let writer_client_max = client_max_packet.clone();
let writer_handle = tokio::spawn(async move {
let _permit = writer_permit;
while let Some(bytes) = out_rx.recv().await {
let n = bytes.len();
let limit = writer_client_max.load(std::sync::atomic::Ordering::SeqCst) as usize;
if limit > 0 && n > limit {
Log::new(Some(&writer_status_tx)).warn(format!(
"MQTT dropped a {n}-byte packet (type {}) for connection {connection_id}: \
the client's Maximum Packet Size is {limit} \
decision=dropped_over_client_max_packet_size",
bytes.first().map(|b| b >> 4).unwrap_or(0)
));
continue;
}
let mut guard = writer_write_half.lock().await;
if let Err(e) = guard.write_all(&bytes).await {
Log::new(Some(&writer_status_tx)).debug(format!("MQTT write failed: {}", e));
break;
}
let _ = guard.flush().await;
drop(guard);
writer_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(n as u64),
None,
Some(1),
)
.await;
}
});
let now = crate::utils::clock::Instant::now();
let conn_state = ConnectionState {
id: connection_id,
remote_addr: peer_addr,
local_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!({
"client_id": serde_json::Value::Null,
})),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let protocol = Arc::new(MqttProtocol::for_connection(
server_id,
connection_id,
out_tx.clone(),
status_tx.clone(),
));
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,
protocol.clone(),
app_state.clone(),
server_id,
connection_id.as_u32(),
write_half.clone(),
status_tx.clone(),
);
let mut session = Session::default();
let mut buffer: Vec<u8> = Vec::with_capacity(1024);
let mut chunk = vec![0u8; 4096];
let close_reason = loop {
let bound = match session.keep_alive {
None => bounds.connect,
Some(ka) => session_idle_bound(ka, bounds.idle_without_keep_alive),
};
let read = match tokio::time::timeout(bound, read_half.read(&mut chunk)).await {
Ok(read) => read,
Err(_) => {
Log::new(Some(&status_tx)).info(format!(
"MQTT connection {} from {} sent nothing for {}ms decision=idle_timeout ({}); closing",
connection_id,
peer_addr,
bound.as_millis(),
match session.keep_alive {
None => "no CONNECT".to_string(),
Some(0) => "keep-alive 0".to_string(),
Some(ka) => format!("1.5x keep-alive {}s", ka),
}
));
break "idle deadline expired";
}
};
let n = match read {
Ok(0) => break "peer closed the connection",
Ok(n) => {
app_state
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
n
}
Err(e) => {
debug!("MQTT read error from {}: {}", peer_addr, e);
break "read error";
}
};
buffer.extend_from_slice(chunk.get(..n).unwrap_or(&[]));
loop {
let packet = match take_packet(&mut buffer, max_packet_size) {
Ok(Some(p)) => p,
Ok(None) => break, Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT protocol error from {} decision=protocol_error: {}",
peer_addr, e
));
session.refuse(&out_tx, e.reason_code());
finish_connection(
protocol,
out_tx,
writer_handle,
&app_state,
server_id,
connection_id,
&session.client_id,
&status_tx,
)
.await;
return Ok(());
}
};
trace!(
"MQTT packet type={} flags={:#x} len={} from {}",
packet.packet_type,
packet.flags,
packet.body.len(),
peer_addr
);
let keep_open = dispatch_packet(
packet,
&mut session,
&client_max_packet,
max_packet_size,
&llm_client,
&app_state,
&status_tx,
server_id,
connection_id,
protocol.clone(),
&out_tx,
)
.await;
if !keep_open {
finish_connection(
protocol,
out_tx,
writer_handle,
&app_state,
server_id,
connection_id,
&session.client_id,
&status_tx,
)
.await;
return Ok(());
}
}
};
debug!(
"MQTT connection {} closing: {}",
connection_id, close_reason
);
Log::new(Some(&status_tx)).info(format!(
"MQTT client {} disconnected ({})",
session.client_id.as_deref().unwrap_or("<unidentified>"),
close_reason
));
finish_connection(
protocol,
out_tx,
writer_handle,
&app_state,
server_id,
connection_id,
&session.client_id,
&status_tx,
)
.await;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn finish_connection(
protocol: Arc<MqttProtocol>,
out_tx: mpsc::UnboundedSender<Vec<u8>>,
writer_handle: tokio::task::JoinHandle<()>,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
client_id: &Option<String>,
status_tx: &mpsc::UnboundedSender<String>,
) {
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
cleanup(app_state, server_id, connection_id, client_id, status_tx).await;
drop(protocol);
drop(out_tx);
if tokio::time::timeout(std::time::Duration::from_secs(5), writer_handle)
.await
.is_err()
{
warn!(
"MQTT connection {}: writer did not finish within 5s, abandoning it",
connection_id
);
}
}
async fn cleanup(
app_state: &AppState,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
client_id: &Option<String>,
status_tx: &mpsc::UnboundedSender<String>,
) {
if let Some(id) = client_id {
actions::unregister_client(server_id, id);
}
app_state
.update_connection_status(server_id, connection_id, ConnectionStatus::Closed)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
}
#[allow(clippy::too_many_arguments)]
async fn dispatch_packet(
packet: RawPacket,
session: &mut Session,
client_max_packet: &std::sync::atomic::AtomicU32,
max_packet_size: usize,
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
protocol: Arc<MqttProtocol>,
out_tx: &mpsc::UnboundedSender<Vec<u8>>,
) -> bool {
match packet.packet_type {
PKT_CONNECT => {
let connect = match parse_connect(&packet.body) {
Ok(c) => c,
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT malformed CONNECT from client decision=protocol_error, closing: {e}"
));
if let Some(code) = e.v5_connack_code() {
let _ =
out_tx.send(build_connack_v5(code, false, &v5::PropertyWriter::new()));
}
return false;
}
};
if session.client_id.is_some() {
Log::new(Some(&status_tx))
.warn("MQTT duplicate CONNECT decision=protocol_error, closing connection");
session.refuse(out_tx, 0x82);
return false;
}
session.v5 = connect.protocol_level == 5;
protocol.set_v5(session.v5);
let wants_enhanced_auth =
session.v5 && connect.properties.text(v5::AUTHENTICATION_METHOD).is_some();
let assigned = connect.client_id.is_empty();
let effective_id = if assigned {
format!("anon-{}", connection_id.as_u32())
} else {
connect.client_id.clone()
};
session.client_id = Some(effective_id.clone());
session.keep_alive = Some(connect.keep_alive);
if session.v5 {
if let Some(limit) = connect.properties.int(v5::MAXIMUM_PACKET_SIZE) {
client_max_packet.store(limit, std::sync::atomic::Ordering::SeqCst);
}
let mut auto = v5::PropertyWriter::new();
if assigned {
auto.text(v5::ASSIGNED_CLIENT_IDENTIFIER, &effective_id);
}
auto.u32(v5::MAXIMUM_PACKET_SIZE, max_packet_size as u32)
.u16(v5::TOPIC_ALIAS_MAXIMUM, TOPIC_ALIAS_MAX);
protocol.set_connack_properties(auto);
}
actions::register_client(server_id, &effective_id, out_tx.clone(), session.v5);
protocol.set_client_id(&effective_id);
app_state
.with_server_mut(server_id, |server| {
if let Some(conn) = server.connections.get_mut(&connection_id) {
conn.protocol_info = ProtocolConnectionInfo::new(serde_json::json!({
"client_id": effective_id,
"protocol_version": version_name(connect.protocol_level),
}));
}
})
.await;
info!(
"MQTT client '{}' connected (v{}, clean_session={}, keep_alive={}s)",
effective_id,
version_name(connect.protocol_level),
connect.clean_session,
connect.keep_alive
);
Log::new(Some(&status_tx)).info(format!(
"MQTT CONNECT from client '{}' (MQTT {}, user={}, clean_session={})",
effective_id,
version_name(connect.protocol_level),
connect.username.as_deref().unwrap_or("-"),
connect.clean_session
));
let mut data = serde_json::json!({
"client_id": effective_id,
"username": connect.username,
"has_password": connect.has_password,
"clean_session": connect.clean_session,
"keep_alive": connect.keep_alive,
"protocol_name": connect.protocol_name,
"protocol_level": connect.protocol_level,
"protocol_version": version_name(connect.protocol_level),
"will_topic": connect.will_topic,
"will_message": connect.will_message,
});
if session.v5 {
data["properties"] = connect.properties.to_json();
data["will_properties"] = connect.will_properties.to_json();
data["assigned_client_id"] = serde_json::json!(assigned);
}
let event = Event::new(&MQTT_CONNECT_EVENT, data);
let outcome = run_llm(
llm_client,
app_state,
server_id,
connection_id,
&event,
protocol.as_ref(),
status_tx,
Some(PKT_CONNACK),
)
.await;
match outcome {
LlmOutcome::Closed => false,
LlmOutcome::Responded => !protocol.connack_refused(),
LlmOutcome::Silent if wants_enhanced_auth => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT CONNECT from '{}' names an authentication method and the handler \
sent no mqtt_connack decision=fail_closed_auth_method, refusing with \
CONNACK 0x8C",
effective_id
));
let _ = out_tx.send(build_connack_v5(0x8C, false, &v5::PropertyWriter::new()));
false
}
LlmOutcome::Silent => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT no mqtt_connack from handler for '{}'; accepting by default \
(CONNACK 0 — an accepted session nothing decided)",
effective_id
));
let _ = out_tx.send(if session.v5 {
build_connack_v5(0, false, &protocol.connack_properties())
} else {
build_connack(0, false)
});
true
}
LlmOutcome::Failed(failure) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT refusing CONNECT from '{}' with CONNACK {} (server {}): \
backend failed",
effective_id,
if session.v5 { "0x88/0x89" } else { "3" },
if failure.is_overloaded() {
"busy"
} else {
"unavailable"
}
));
let _ = out_tx.send(if session.v5 {
let code = if failure.is_overloaded() { 0x89 } else { 0x88 };
let mut props = v5::PropertyWriter::new();
props.text(v5::REASON_STRING, failure.text());
build_connack_v5(code, false, &props)
} else {
build_connack(CONNACK_SERVER_UNAVAILABLE, false)
});
false
}
}
}
PKT_PUBLISH => {
let qos = (packet.flags >> 1) & 0x03;
if qos > 2 {
Log::new(Some(&status_tx))
.warn("MQTT PUBLISH with invalid QoS 3 decision=protocol_error, closing");
session.refuse(out_tx, 0x81);
return false;
}
let mut publish = match parse_publish(&packet.body, qos, session.v5) {
Ok(p) => p,
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT malformed PUBLISH decision=protocol_error, closing connection: {e}"
));
session.refuse(out_tx, e.reason_code());
return false;
}
};
if let Some(alias) = publish.properties.int(v5::TOPIC_ALIAS) {
let alias = alias as u16;
if alias == 0 || alias > TOPIC_ALIAS_MAX {
Log::new(Some(&status_tx)).warn(format!(
"MQTT PUBLISH with topic alias {alias} outside 1..={TOPIC_ALIAS_MAX} \
decision=protocol_error, closing"
));
session.refuse(out_tx, 0x94);
return false;
}
if publish.topic.is_empty() {
match session.aliases.get(&alias) {
Some(topic) => publish.topic = topic.clone(),
None => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT PUBLISH uses topic alias {alias}, which this connection \
never defined decision=protocol_error, closing"
));
session.refuse(out_tx, 0x82);
return false;
}
}
} else {
session.aliases.insert(alias, publish.topic.clone());
}
} else if session.v5 && publish.topic.is_empty() {
Log::new(Some(&status_tx)).warn(
"MQTT PUBLISH with an empty topic and no topic alias \
decision=protocol_error, closing",
);
session.refuse(out_tx, 0x82);
return false;
}
let (payload, payload_is_text) = decode_payload(&publish.payload);
let name = session
.client_id
.clone()
.unwrap_or_else(|| "<no CONNECT>".into());
Log::new(Some(&status_tx)).debug(format!(
"MQTT PUBLISH from '{}' topic='{}' qos={} retain={} {} bytes",
name,
publish.topic,
qos,
(packet.flags & 0x01) == 1,
publish.payload.len()
));
let mut data = serde_json::json!({
"client_id": name,
"topic": publish.topic,
"payload": payload,
"payload_is_text": payload_is_text,
"payload_size": publish.payload.len(),
"qos": qos,
"retain": (packet.flags & 0x01) == 1,
"duplicate": (packet.flags & 0x08) != 0,
"packet_id": publish.packet_id,
"connected_clients": actions::list_clients(server_id),
});
if session.v5 {
data["properties"] = publish.properties.to_json();
}
let event = Event::new(&MQTT_PUBLISH_EVENT, data);
let outcome = run_llm(
llm_client,
app_state,
server_id,
connection_id,
&event,
protocol.as_ref(),
status_tx,
match qos {
1 => Some(PKT_PUBACK),
2 => Some(PKT_PUBREC),
_ => None,
},
)
.await;
match outcome {
LlmOutcome::Closed => false,
LlmOutcome::Responded => true,
LlmOutcome::Silent => {
match qos {
1 => {
let _ = out_tx.send(build_ack(PKT_PUBACK, publish.packet_id));
}
2 => {
let _ = out_tx.send(build_ack(PKT_PUBREC, publish.packet_id));
}
_ => {}
}
true
}
LlmOutcome::Failed(failure) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT closing connection: backend failed on PUBLISH (qos={}, id={}), \
acknowledging it would claim delivery",
qos, publish.packet_id
));
if session.v5 {
let mut props = v5::PropertyWriter::new();
props.text(v5::REASON_STRING, failure.text());
let code = if failure.is_overloaded() { 0x89 } else { 0x80 };
let _ = out_tx.send(build_disconnect_v5(code, &props));
}
false
}
}
}
PKT_PUBREL => {
let packet_id = read_u16_at(&packet.body, 0).unwrap_or(0);
trace!("MQTT PUBREL id={} -> PUBCOMP", packet_id);
let _ = out_tx.send(build_ack(PKT_PUBCOMP, packet_id));
true
}
PKT_SUBSCRIBE => {
let sub = match parse_subscribe(&packet.body, session.v5) {
Ok(s) => s,
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT malformed SUBSCRIBE decision=protocol_error, closing connection: {e}"
));
session.refuse(out_tx, e.reason_code());
return false;
}
};
if sub.topics.is_empty() {
Log::new(Some(&status_tx))
.warn("MQTT SUBSCRIBE with no topic filter decision=protocol_error, closing");
session.refuse(out_tx, 0x82);
return false;
}
let name = session
.client_id
.clone()
.unwrap_or_else(|| "<no CONNECT>".into());
Log::new(Some(&status_tx)).info(format!(
"MQTT SUBSCRIBE from '{}': {}",
name,
sub.topics
.iter()
.map(|(f, q)| format!("{} (qos {})", f, q))
.collect::<Vec<_>>()
.join(", ")
));
let topics: Vec<serde_json::Value> = sub
.topics
.iter()
.zip(sub.options.iter())
.map(|((filter, qos), opts)| {
let mut t = serde_json::json!({ "filter": filter, "qos": qos });
if session.v5 {
t["no_local"] = serde_json::json!(opts.no_local);
t["retain_as_published"] = serde_json::json!(opts.retain_as_published);
t["retain_handling"] = serde_json::json!(opts.retain_handling);
}
t
})
.collect();
let mut data = serde_json::json!({
"client_id": name,
"packet_id": sub.packet_id,
"topics": topics,
"answer_with": subscribe_answer_with(sub.packet_id, &sub.topics),
});
if session.v5 {
data["properties"] = sub.properties.to_json();
}
let event = Event::new(&MQTT_SUBSCRIBE_EVENT, data);
let outcome = run_llm(
llm_client,
app_state,
server_id,
connection_id,
&event,
protocol.as_ref(),
status_tx,
Some(PKT_SUBACK),
)
.await;
match outcome {
LlmOutcome::Closed => false,
LlmOutcome::Responded => true,
LlmOutcome::Silent => {
let granted: Vec<u8> = sub.topics.iter().map(|(_, q)| *q).collect();
Log::new(Some(&status_tx)).warn(format!(
"MQTT no mqtt_suback from handler; granting requested QoS to '{}'",
name
));
let _ = out_tx.send(if session.v5 {
build_suback_v5(sub.packet_id, &granted, &v5::PropertyWriter::new())
} else {
build_suback(sub.packet_id, &granted)
});
true
}
LlmOutcome::Failed(failure) => {
let refused = vec![SUBACK_FAILURE; sub.topics.len()];
Log::new(Some(&status_tx)).warn(format!(
"MQTT refusing all {} subscription(s) from '{}' with SUBACK 0x80: \
backend failed",
sub.topics.len(),
name
));
let _ = out_tx.send(if session.v5 {
let mut props = v5::PropertyWriter::new();
props.text(v5::REASON_STRING, failure.text());
build_suback_v5(sub.packet_id, &refused, &props)
} else {
build_suback(sub.packet_id, &refused)
});
true
}
}
}
PKT_UNSUBSCRIBE => {
let unsub = match parse_unsubscribe(&packet.body, session.v5) {
Ok(u) => u,
Err(e) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT malformed UNSUBSCRIBE decision=protocol_error, closing connection: \
{e}"
));
session.refuse(out_tx, e.reason_code());
return false;
}
};
let name = session
.client_id
.clone()
.unwrap_or_else(|| "<no CONNECT>".into());
Log::new(Some(&status_tx)).info(format!(
"MQTT UNSUBSCRIBE from '{}': {}",
name,
unsub.topics.join(", ")
));
protocol.note_unsubscribe(unsub.packet_id, unsub.topics.len());
let mut data = serde_json::json!({
"client_id": name,
"packet_id": unsub.packet_id,
"topics": unsub.topics,
});
if session.v5 {
data["properties"] = unsub.properties.to_json();
}
let event = Event::new(&MQTT_UNSUBSCRIBE_EVENT, data);
let outcome = run_llm(
llm_client,
app_state,
server_id,
connection_id,
&event,
protocol.as_ref(),
status_tx,
Some(PKT_UNSUBACK),
)
.await;
match outcome {
LlmOutcome::Closed => false,
LlmOutcome::Responded => true,
LlmOutcome::Silent => {
let _ = out_tx.send(if session.v5 {
build_unsuback_v5(
unsub.packet_id,
&vec![0x00; unsub.topics.len()],
&v5::PropertyWriter::new(),
)
} else {
build_ack(PKT_UNSUBACK, unsub.packet_id)
});
true
}
LlmOutcome::Failed(failure) if session.v5 => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT refusing UNSUBSCRIBE (id={}) from '{}' with UNSUBACK 0x80: \
backend failed",
unsub.packet_id, name
));
let mut props = v5::PropertyWriter::new();
props.text(v5::REASON_STRING, failure.text());
let _ = out_tx.send(build_unsuback_v5(
unsub.packet_id,
&vec![0x80; unsub.topics.len()],
&props,
));
true
}
LlmOutcome::Failed(_) => {
Log::new(Some(&status_tx)).warn(format!(
"MQTT closing connection: backend failed on UNSUBSCRIBE (id={}), \
acknowledging it would claim the subscriptions were removed",
unsub.packet_id
));
false
}
}
}
PKT_PINGREQ => {
trace!("MQTT PINGREQ -> PINGRESP");
let _ = out_tx.send(vec![PKT_PINGRESP << 4, 0x00]);
true
}
PKT_DISCONNECT => {
let (reason, detail) = if session.v5 {
let reason = packet.body.first().copied().unwrap_or(0);
let mut pos = 1usize;
let text = if packet.body.len() > 1 {
v5::read_properties(&packet.body, &mut pos)
.ok()
.and_then(|p| p.text(v5::REASON_STRING).map(str::to_string))
} else {
None
};
(Some(reason), text)
} else {
(None, None)
};
Log::new(Some(&status_tx)).info(format!(
"MQTT client '{}' sent DISCONNECT{}{}",
session.client_id.as_deref().unwrap_or("<unidentified>"),
reason
.map(|r| format!(" reason=0x{r:02x}"))
.unwrap_or_default(),
detail.map(|d| format!(" ({d})")).unwrap_or_default()
));
false
}
PKT_AUTH if session.v5 => {
Log::new(Some(&status_tx)).warn(
"MQTT AUTH packet: enhanced authentication is not supported \
decision=protocol_error, closing",
);
session.refuse(out_tx, 0x82);
false
}
other => {
trace!("MQTT ignoring inbound packet type {}", other);
true
}
}
}
pub fn version_name(level: u8) -> &'static str {
match level {
3 => "3.1",
4 => "3.1.1",
5 => "5.0",
_ => "unknown",
}
}
pub fn subscribe_answer_with(packet_id: u16, topics: &[(String, u8)]) -> String {
let filters: Vec<&str> = topics.iter().map(|(f, _)| f.as_str()).collect();
let granted: Vec<String> = topics.iter().map(|(_, q)| q.to_string()).collect();
format!(
"mqtt_suback with packet_id {packet_id} and granted_qos [{}] (one entry per filter; \
128 refuses one). Then, for each topic matching {:?} that your instruction gives a \
retained message or current value, mqtt_publish with that topic, the value as \
payload and retain true - the subscriber is waiting for it. Publish nothing if the \
instruction gives none.",
granted.join(", "),
filters
)
}
enum LlmOutcome {
Responded,
Silent,
Closed,
Failed(crate::utils::wire_failure::WireFailure),
}
#[allow(clippy::too_many_arguments)]
async fn run_llm(
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
event: &Event,
protocol: &MqttProtocol,
status_tx: &mpsc::UnboundedSender<String>,
required_reply: Option<u8>,
) -> LlmOutcome {
protocol.clear_written_types();
match call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
event,
protocol,
)
.await
{
Ok(result) => {
let client = event
.data
.get("client_id")
.and_then(|v| v.as_str())
.unwrap_or("<unidentified>");
let log = Log::new(Some(&status_tx));
if result
.protocol_results
.iter()
.any(|r| matches!(r, ActionResult::CloseConnection))
{
log.info(format!(
"MQTT {} from '{}' ({}) decision=model_reject (close_connection)",
event.event_type.id, client, connection_id
));
return LlmOutcome::Closed;
}
let answered = match required_reply {
None => !result.raw_actions.is_empty(),
Some(packet_type) => protocol.wrote_packet_type(packet_type),
};
if answered {
log.info(format!(
"MQTT {} from '{}' ({}) decision=model_answer",
event.event_type.id, client, connection_id
));
LlmOutcome::Responded
} else {
log.warn(format!(
"MQTT {} from '{}' ({}) decision=model_silent actions={} — the protocol \
default applies",
event.event_type.id,
client,
connection_id,
result.raw_actions.len()
));
match required_reply {
None => LlmOutcome::Responded,
Some(_) => LlmOutcome::Silent,
}
}
}
Err(e) => {
let decision = if crate::llm::is_overload_error(&e) {
"fail_closed_llm_overloaded"
} else {
"fail_closed_llm_error"
};
let client = event
.data
.get("client_id")
.and_then(|v| v.as_str())
.unwrap_or("<unidentified>");
Log::new(Some(&status_tx)).error(format!(
"MQTT {} from '{}' ({}) decision={}: {}",
event.event_type.id, client, connection_id, decision, e
));
LlmOutcome::Failed(crate::utils::wire_failure::WireFailure::classify(&e))
}
}
}
#[derive(Debug, Clone)]
pub struct PacketError {
pub reason: u8,
pub message: String,
pub v5_connect: bool,
}
impl PacketError {
fn malformed(message: impl Into<String>) -> Self {
Self {
reason: 0x81,
message: message.into(),
v5_connect: false,
}
}
fn protocol(message: impl Into<String>) -> Self {
Self {
reason: 0x82,
message: message.into(),
v5_connect: false,
}
}
pub fn reason_code(&self) -> u8 {
self.reason
}
pub fn v5_connack_code(&self) -> Option<u8> {
self.v5_connect.then_some(self.reason)
}
}
impl std::fmt::Display for PacketError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl From<v5::PropertyError> for PacketError {
fn from(e: v5::PropertyError) -> Self {
match e {
v5::PropertyError::Malformed(_) => PacketError::malformed(e.to_string()),
v5::PropertyError::Duplicate(_) | v5::PropertyError::Protocol(_) => {
PacketError::protocol(e.to_string())
}
}
}
}
fn take_packet(
buffer: &mut Vec<u8>,
max_packet_size: usize,
) -> std::result::Result<Option<RawPacket>, PacketError> {
let Some(&first) = buffer.first() else {
return Ok(None);
};
let packet_type = first >> 4;
if packet_type == 0 {
return Err(PacketError::malformed(format!(
"invalid packet type {}",
packet_type
)));
}
let (remaining, len_bytes) = match decode_remaining_length(&buffer[1..]) {
Some(v) => v,
None => {
if buffer.len() > 5 {
return Err(PacketError::malformed("malformed remaining length"));
}
return Ok(None);
}
};
if remaining > max_packet_size {
return Err(PacketError {
reason: 0x95,
message: format!(
"packet of {} bytes exceeds max_packet_size {}",
remaining, max_packet_size
),
v5_connect: false,
});
}
let total = 1 + len_bytes + remaining;
if buffer.len() < total {
return Ok(None);
}
let body = buffer[1 + len_bytes..total].to_vec();
buffer.drain(..total);
Ok(Some(RawPacket {
packet_type,
flags: first & 0x0F,
body,
}))
}
fn decode_remaining_length(buf: &[u8]) -> Option<(usize, usize)> {
let mut value: usize = 0;
let mut multiplier: usize = 1;
for i in 0..4 {
let byte = *buf.get(i)?;
value += (byte & 0x7F) as usize * multiplier;
if byte & 0x80 == 0 {
return Some((value, i + 1));
}
multiplier *= 128;
}
None
}
fn read_u16_at(buf: &[u8], pos: usize) -> Option<u16> {
let hi = *buf.get(pos)?;
let lo = *buf.get(pos + 1)?;
Some(u16::from_be_bytes([hi, lo]))
}
fn read_string(buf: &[u8], pos: &mut usize) -> Option<String> {
let len = read_u16_at(buf, *pos)? as usize;
let start = pos.checked_add(2)?;
let end = start.checked_add(len)?;
let bytes = buf.get(start..end)?;
*pos = end;
Some(String::from_utf8_lossy(bytes).into_owned())
}
fn read_bytes(buf: &[u8], pos: &mut usize) -> Option<Vec<u8>> {
let len = read_u16_at(buf, *pos)? as usize;
let start = pos.checked_add(2)?;
let end = start.checked_add(len)?;
let bytes = buf.get(start..end)?.to_vec();
*pos = end;
Some(bytes)
}
fn decode_payload(bytes: &[u8]) -> (String, bool) {
match std::str::from_utf8(bytes) {
Ok(s) => (s.to_string(), true),
Err(_) => (String::from_utf8_lossy(bytes).into_owned(), false),
}
}
pub struct ConnectPacket {
pub protocol_name: String,
pub protocol_level: u8,
pub clean_session: bool,
pub keep_alive: u16,
pub client_id: String,
pub username: Option<String>,
pub has_password: bool,
pub will_topic: Option<String>,
pub will_message: Option<String>,
pub properties: v5::Properties,
pub will_properties: v5::Properties,
}
pub fn parse_connect(body: &[u8]) -> std::result::Result<ConnectPacket, PacketError> {
let short = || PacketError::malformed("CONNECT is truncated");
let mut pos = 0usize;
let protocol_name = read_string(body, &mut pos).ok_or_else(short)?;
let protocol_level = *body.get(pos).ok_or_else(short)?;
pos += 1;
let v5 = protocol_level == 5;
let tag = |mut e: PacketError| {
e.v5_connect = v5;
e
};
let flags = *body.get(pos).ok_or_else(short).map_err(tag)?;
pos += 1;
let keep_alive = read_u16_at(body, pos).ok_or_else(short).map_err(tag)?;
pos += 2;
let properties = if v5 {
let props = v5::read_properties(body, &mut pos)
.map_err(PacketError::from)
.map_err(tag)?;
v5::forbid(&props, &[v5::SUBSCRIPTION_IDENTIFIER], "CONNECT")
.map_err(PacketError::from)
.map_err(tag)?;
if props.int(v5::MAXIMUM_PACKET_SIZE) == Some(0) {
return Err(tag(PacketError::protocol("maximum packet size 0")));
}
props
} else {
v5::Properties::default()
};
let client_id = read_string(body, &mut pos).ok_or_else(short).map_err(tag)?;
let mut will_properties = v5::Properties::default();
let (will_topic, will_message) = if flags & 0x04 != 0 {
if v5 {
will_properties = v5::read_properties(body, &mut pos)
.map_err(PacketError::from)
.map_err(tag)?;
v5::forbid(
&will_properties,
&[v5::SUBSCRIPTION_IDENTIFIER],
"Will Properties",
)
.map_err(PacketError::from)
.map_err(tag)?;
}
let topic = read_string(body, &mut pos).ok_or_else(short).map_err(tag)?;
let message = read_bytes(body, &mut pos).ok_or_else(short).map_err(tag)?;
(Some(topic), Some(decode_payload(&message).0))
} else {
(None, None)
};
let username = if flags & 0x80 != 0 {
Some(read_string(body, &mut pos).ok_or_else(short).map_err(tag)?)
} else {
None
};
let has_password = flags & 0x40 != 0;
Ok(ConnectPacket {
protocol_name,
protocol_level,
clean_session: flags & 0x02 != 0,
keep_alive,
client_id,
username,
has_password,
will_topic,
will_message,
properties,
will_properties,
})
}
pub struct PublishPacket {
pub topic: String,
pub packet_id: u16,
pub payload: Vec<u8>,
pub properties: v5::Properties,
}
pub fn parse_publish(
body: &[u8],
qos: u8,
v5: bool,
) -> std::result::Result<PublishPacket, PacketError> {
let short = || PacketError::malformed("PUBLISH is truncated");
let mut pos = 0usize;
let topic = read_string(body, &mut pos).ok_or_else(short)?;
let packet_id = if qos > 0 {
let id = read_u16_at(body, pos).ok_or_else(short)?;
pos += 2;
id
} else {
0
};
let properties = if v5 {
let props = v5::read_properties(body, &mut pos)?;
v5::forbid(&props, &[v5::SUBSCRIPTION_IDENTIFIER], "a client's PUBLISH")?;
props
} else {
v5::Properties::default()
};
let payload = body.get(pos..).ok_or_else(short)?.to_vec();
Ok(PublishPacket {
topic,
packet_id,
payload,
properties,
})
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SubscriptionOptions {
pub no_local: bool,
pub retain_as_published: bool,
pub retain_handling: u8,
}
pub struct SubscribePacket {
pub packet_id: u16,
pub topics: Vec<(String, u8)>,
pub options: Vec<SubscriptionOptions>,
pub properties: v5::Properties,
}
pub fn parse_subscribe(body: &[u8], v5: bool) -> std::result::Result<SubscribePacket, PacketError> {
let short = || PacketError::malformed("SUBSCRIBE is truncated");
let mut pos = 0usize;
let packet_id = read_u16_at(body, pos).ok_or_else(short)?;
pos += 2;
let properties = if v5 {
v5::read_properties(body, &mut pos)?
} else {
v5::Properties::default()
};
let mut topics = Vec::new();
let mut options = Vec::new();
while pos < body.len() {
let filter = read_string(body, &mut pos).ok_or_else(short)?;
let byte = *body.get(pos).ok_or_else(short)?;
pos += 1;
let qos = byte & 0x03;
let mut opts = SubscriptionOptions::default();
if v5 {
if qos == 3 || (byte >> 4) & 0x03 == 3 || byte & 0xC0 != 0 {
return Err(PacketError::malformed(format!(
"invalid subscription options byte 0x{byte:02x} for '{filter}'"
)));
}
opts = SubscriptionOptions {
no_local: byte & 0x04 != 0,
retain_as_published: byte & 0x08 != 0,
retain_handling: (byte >> 4) & 0x03,
};
}
topics.push((filter, qos));
options.push(opts);
}
Ok(SubscribePacket {
packet_id,
topics,
options,
properties,
})
}
pub struct UnsubscribePacket {
pub packet_id: u16,
pub topics: Vec<String>,
pub properties: v5::Properties,
}
pub fn parse_unsubscribe(
body: &[u8],
v5: bool,
) -> std::result::Result<UnsubscribePacket, PacketError> {
let short = || PacketError::malformed("UNSUBSCRIBE is truncated");
let mut pos = 0usize;
let packet_id = read_u16_at(body, pos).ok_or_else(short)?;
pos += 2;
let properties = if v5 {
let props = v5::read_properties(body, &mut pos)?;
v5::forbid(&props, &[v5::SUBSCRIPTION_IDENTIFIER], "UNSUBSCRIBE")?;
props
} else {
v5::Properties::default()
};
let mut topics = Vec::new();
while pos < body.len() {
topics.push(read_string(body, &mut pos).ok_or_else(short)?);
}
Ok(UnsubscribePacket {
packet_id,
topics,
properties,
})
}
pub fn encode_remaining_length(mut len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(4);
loop {
let mut byte = (len % 128) as u8;
len /= 128;
if len > 0 {
byte |= 0x80;
}
out.push(byte);
if len == 0 || out.len() == 4 {
break;
}
}
out
}
pub fn build_connack(return_code: u8, session_present: bool) -> Vec<u8> {
vec![
PKT_CONNACK << 4,
0x02,
if session_present { 0x01 } else { 0x00 },
return_code,
]
}
pub fn build_ack(packet_type: u8, packet_id: u16) -> Vec<u8> {
let id = packet_id.to_be_bytes();
let header = if packet_type == PKT_PUBREL {
(packet_type << 4) | 0x02
} else {
packet_type << 4
};
vec![header, 0x02, id[0], id[1]]
}
pub fn build_suback(packet_id: u16, granted_qos: &[u8]) -> Vec<u8> {
let mut body = packet_id.to_be_bytes().to_vec();
body.extend_from_slice(granted_qos);
let mut out = vec![PKT_SUBACK << 4];
out.extend_from_slice(&encode_remaining_length(body.len()));
out.extend_from_slice(&body);
out
}
pub fn build_publish(topic: &str, payload: &str, qos: u8, retain: bool, packet_id: u16) -> Vec<u8> {
let topic_bytes = topic.as_bytes();
let payload_bytes = payload.as_bytes();
let mut body = Vec::with_capacity(2 + topic_bytes.len() + 2 + payload_bytes.len());
body.extend_from_slice(&(topic_bytes.len() as u16).to_be_bytes());
body.extend_from_slice(topic_bytes);
if qos > 0 {
body.extend_from_slice(&packet_id.to_be_bytes());
}
body.extend_from_slice(payload_bytes);
let mut flags = (qos & 0x03) << 1;
if retain {
flags |= 0x01;
}
let mut out = vec![(PKT_PUBLISH << 4) | flags];
out.extend_from_slice(&encode_remaining_length(body.len()));
out.extend_from_slice(&body);
out
}
fn frame(first_byte: u8, body: &[u8]) -> Vec<u8> {
let mut out = vec![first_byte];
out.extend_from_slice(&encode_remaining_length(body.len()));
out.extend_from_slice(body);
out
}
pub fn build_connack_v5(reason: u8, session_present: bool, props: &v5::PropertyWriter) -> Vec<u8> {
let mut body = vec![u8::from(session_present), reason];
body.extend_from_slice(&props.finish());
frame(PKT_CONNACK << 4, &body)
}
pub fn build_ack_v5(
packet_type: u8,
packet_id: u16,
reason: u8,
props: &v5::PropertyWriter,
) -> Vec<u8> {
if reason == 0 && props.is_empty() {
return build_ack(packet_type, packet_id);
}
let mut body = packet_id.to_be_bytes().to_vec();
body.push(reason);
body.extend_from_slice(&props.finish());
let first = if packet_type == PKT_PUBREL {
(packet_type << 4) | 0x02
} else {
packet_type << 4
};
frame(first, &body)
}
pub fn build_suback_v5(packet_id: u16, codes: &[u8], props: &v5::PropertyWriter) -> Vec<u8> {
let mut body = packet_id.to_be_bytes().to_vec();
body.extend_from_slice(&props.finish());
body.extend_from_slice(codes);
frame(PKT_SUBACK << 4, &body)
}
pub fn build_unsuback_v5(packet_id: u16, codes: &[u8], props: &v5::PropertyWriter) -> Vec<u8> {
let mut body = packet_id.to_be_bytes().to_vec();
body.extend_from_slice(&props.finish());
body.extend_from_slice(codes);
frame(PKT_UNSUBACK << 4, &body)
}
pub fn build_disconnect_v5(reason: u8, props: &v5::PropertyWriter) -> Vec<u8> {
let mut body = vec![reason];
body.extend_from_slice(&props.finish());
frame(PKT_DISCONNECT << 4, &body)
}
pub fn build_publish_v5(
topic: &str,
payload: &[u8],
qos: u8,
retain: bool,
packet_id: u16,
props: &v5::PropertyWriter,
) -> Vec<u8> {
let mut body = Vec::with_capacity(2 + topic.len() + 2 + 8 + payload.len());
body.extend_from_slice(&(topic.len() as u16).to_be_bytes());
body.extend_from_slice(topic.as_bytes());
if qos > 0 {
body.extend_from_slice(&packet_id.to_be_bytes());
}
body.extend_from_slice(&props.finish());
body.extend_from_slice(payload);
let mut flags = (qos & 0x03) << 1;
if retain {
flags |= 0x01;
}
frame((PKT_PUBLISH << 4) | flags, &body)
}
#[derive(Clone, Debug)]
pub struct OutPublish {
pub topic: String,
pub payload: String,
pub qos: u8,
pub retain: bool,
pub packet_id: u16,
pub properties: v5::PropertyWriter,
}
impl OutPublish {
pub fn encode(&self, v5: bool) -> Vec<u8> {
if v5 {
build_publish_v5(
&self.topic,
self.payload.as_bytes(),
self.qos,
self.retain,
self.packet_id,
&self.properties,
)
} else {
build_publish(
&self.topic,
&self.payload,
self.qos,
self.retain,
self.packet_id,
)
}
}
}