pub mod actions;
use anyhow::{Context, Result};
use bytes::Bytes;
use pcap::{Capture, Device};
use std::sync::Arc;
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::state::app_state::AppState;
use crate::utils::truncate_for_log;
use crate::{console_debug, console_error, console_info, console_trace, console_warn};
use actions::{DataLinkProtocol, DATALINK_PACKET_CAPTURED_EVENT};
pub const MAX_HEX_BYTES_TO_MODEL: usize = 2048;
const MAX_HEX_BYTES_TO_LOG: usize = 512;
const MAX_INFLIGHT_LLM_PACKETS: usize = 32;
pub fn packet_event_data(frame: &[u8]) -> serde_json::Value {
let shown = frame.len().min(MAX_HEX_BYTES_TO_MODEL);
serde_json::json!({
"packet_length": frame.len(),
"packet_hex": hex::encode(&frame[..shown]),
"captured_length": shown,
"truncated": shown < frame.len(),
})
}
pub struct DataLinkServer;
impl DataLinkServer {
pub fn list_devices() -> Result<Vec<Device>> {
Device::list().context("Failed to list network devices")
}
pub fn find_device(name: &str) -> Result<Device> {
let devices = Self::list_devices()?;
devices
.into_iter()
.find(|d| d.name == name)
.ok_or_else(|| anyhow::anyhow!("Device '{}' not found", name))
}
pub async fn spawn_with_llm(
interface: String,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
filter: Option<String>,
server_id: crate::state::ServerId,
) -> Result<String> {
console_info!(
status_tx,
"Starting packet capture on interface: {}",
interface
);
let status_tx_ready = status_tx.clone();
let protocol = Arc::new(DataLinkProtocol::new());
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<Result<()>>();
let stop = crate::utils::StopSignal::new();
let stop_in_loop = stop.clone();
let app_state_reg = app_state.clone();
let inflight = Arc::new(tokio::sync::Semaphore::new(MAX_INFLIGHT_LLM_PACKETS));
let interface_clone = interface.clone();
let protocol_clone = protocol.clone();
tokio::task::spawn_blocking(move || {
let open_capture = || -> Result<pcap::Capture<pcap::Active>> {
let device = Self::find_device(&interface_clone)
.with_context(|| format!("no such capture device '{}'", interface_clone))?;
let mut cap = Capture::from_device(device)
.map(|c| c.promisc(true).snaplen(65535).timeout(1000))
.and_then(|c| c.open())
.with_context(|| {
format!(
"failed to open pcap capture on '{}' (needs root, or \
read access to /dev/bpf* on macOS/BSD, or CAP_NET_RAW on Linux)",
interface_clone
)
})?;
if let Some(ref filter_str) = filter {
cap.filter(filter_str, true)
.with_context(|| format!("invalid BPF filter '{}'", filter_str))?;
}
Ok(cap)
};
let mut cap = match open_capture() {
Ok(cap) => {
let _ = ready_tx.send(Ok(()));
cap
}
Err(e) => {
console_error!(status_tx, "DataLink capture startup failed: {:#}", e);
let _ = ready_tx.send(Err(e));
return;
}
};
let runtime = tokio::runtime::Handle::current();
let mut dropped: u64 = 0;
loop {
if stop_in_loop.is_stopped() {
console_info!(
status_tx,
"DataLink capture on {} stopping",
interface_clone
);
break;
}
match cap.next_packet() {
Ok(packet) => {
let data = Bytes::copy_from_slice(packet.data);
console_debug!(status_tx, "Datalink received {} bytes", data.len());
console_trace!(
status_tx,
"Datalink data (hex): {}",
truncate_for_log(
&hex::encode(&data),
MAX_HEX_BYTES_TO_LOG.saturating_mul(2)
)
);
let permit = match inflight.clone().try_acquire_owned() {
Ok(p) => p,
Err(_) => {
dropped += 1;
if dropped == 1 || dropped % 100 == 0 {
console_warn!(
status_tx,
"DataLink dropped {} captured frame(s) on {}: \
{} already awaiting the model. Narrow the BPF filter \
or use a script/static handler for high-rate capture.",
dropped,
interface_clone,
MAX_INFLIGHT_LLM_PACKETS
);
}
continue;
}
};
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_task_clone = protocol_clone.clone();
runtime.spawn(async move {
let _permit = permit;
let full_len = data.len();
let event = Event::new(
&DATALINK_PACKET_CAPTURED_EVENT,
packet_event_data(&data),
);
debug!("Datalink calling LLM for packet ({} bytes)", full_len);
let _ = status_clone.send(format!(
"[DEBUG] Datalink calling LLM for packet ({} bytes)",
full_len
));
match call_llm(
&llm_clone,
&state_clone,
server_id,
None,
&event,
protocol_task_clone.as_ref(),
)
.await
{
Ok(execution_result) => {
for message in &execution_result.messages {
info!("{}", message);
let _ = status_clone.send(format!("[INFO] {}", message));
}
let named_ignore =
execution_result.raw_actions.iter().any(|a| {
a.get("type").and_then(|v| v.as_str())
== Some("ignore_packet")
});
let decision = if named_ignore {
"model_ignore"
} else if execution_result.raw_actions.is_empty() {
"model_silent"
} else {
"model_analysed"
};
debug!(
"Datalink packet ({} bytes) decision={} ({} actions, \
{} protocol results)",
full_len,
decision,
execution_result.raw_actions.len(),
execution_result.protocol_results.len()
);
let _ = status_clone.send(format!(
"[DEBUG] Datalink packet decision={} ({} actions)",
decision,
execution_result.raw_actions.len()
));
let _ = status_clone.send(format!(
"→ Datalink packet processed: {} bytes",
full_len
));
}
Err(e) => {
let category = if crate::utils::WireFailure::classify(&e)
.is_overloaded()
{
"overloaded"
} else {
"unavailable"
};
warn!(
"Datalink packet ({} bytes) decision=fail_closed_llm_error \
category={}: {}",
full_len, category, e
);
let _ = status_clone.send(format!(
"✗ Datalink packet decision=fail_closed_llm_error \
category={}: {}",
category, e
));
}
}
});
}
Err(pcap::Error::TimeoutExpired) => {
continue;
}
Err(e) => {
console_error!(status_tx, "Packet capture error: {}", e);
break;
}
}
}
if dropped > 0 {
console_warn!(
status_tx,
"DataLink capture on {} ended having dropped {} frame(s) to backpressure",
interface_clone,
dropped
);
}
drop(cap);
});
match ready_rx.await {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(e),
Err(_) => {
return Err(anyhow::anyhow!(
"DataLink capture task on '{}' exited before signalling readiness",
interface
))
}
}
app_state_reg
.register_server_task(server_id, stop.park_task())
.await;
console_info!(status_tx_ready, "DataLink capture active on {}", interface);
Ok(interface)
}
}