1pub mod actions;
26pub mod wire;
27
28use crate::llm::action_helper::call_llm;
29use crate::llm::actions::protocol_trait::ActionResult;
30use crate::llm::ollama_client::OllamaClient;
31use crate::logging::emit::Log;
32use crate::protocol::Event;
33use crate::server::connection::ConnectionId;
34use crate::state::app_state::AppState;
35use anyhow::Result;
36use std::net::SocketAddr;
37use std::sync::Arc;
38use std::time::Duration;
39use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
40use tokio::net::TcpListener;
41use tokio::sync::{mpsc, Mutex};
42
43pub use wire::MAX_DATA_BYTES;
44
45const FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(30);
50
51const IDLE_TIMEOUT: Duration = Duration::from_secs(30);
53
54const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
56
57const INFO_TOO_LARGE: &str = "message is too large";
59const INFO_COMPRESSED: &str = "compressed data is not supported";
60const INFO_BAD_FLAGS: &str = "unsupported protocol flags";
61const INFO_UNSUPPORTED: &str = "unsupported request";
62
63pub struct ZabbixServer;
64
65impl ZabbixServer {
66 pub async fn spawn_with_llm_actions(
67 listen_addr: SocketAddr,
68 llm_client: OllamaClient,
69 app_state: Arc<AppState>,
70 status_tx: mpsc::UnboundedSender<String>,
71 server_id: crate::state::ServerId,
72 first_byte_timeout_secs: Option<u64>,
73 idle_timeout_secs: Option<u64>,
74 ) -> Result<SocketAddr> {
75 let first_timeout = first_byte_timeout_secs
76 .map(Duration::from_secs)
77 .unwrap_or(FIRST_BYTE_TIMEOUT);
78 let idle_timeout = idle_timeout_secs
79 .map(Duration::from_secs)
80 .unwrap_or(IDLE_TIMEOUT);
81 let listener = TcpListener::bind(listen_addr).await?;
82 let local_addr = listener.local_addr()?;
83
84 Log::new(Some(&status_tx)).info(format!("Zabbix trapper listening on {}", local_addr));
85
86 let protocol = Arc::new(actions::ZabbixProtocol::new());
87 let task_registrar = app_state.clone();
88 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
89 let accept_handle = tokio::spawn(async move {
90 loop {
91 match crate::server::accept_bounded::accept_bounded(
95 &listener,
96 &limiter,
97 b"",
98 "Zabbix",
99 Some(&status_tx),
100 )
101 .await
102 {
103 Ok((socket, peer_addr, permit)) => {
104 let connection_id =
105 ConnectionId::new(app_state.get_next_unified_id().await);
106 use crate::state::server::{
107 ConnectionState as ServerConnectionState, ConnectionStatus,
108 ProtocolConnectionInfo,
109 };
110 let now = crate::utils::clock::Instant::now();
111 app_state
112 .add_connection_to_server(
113 server_id,
114 ServerConnectionState {
115 id: connection_id,
116 remote_addr: peer_addr,
117 local_addr,
118 bytes_sent: 0,
119 bytes_received: 0,
120 packets_sent: 0,
121 packets_received: 0,
122 last_activity: now,
123 status: ConnectionStatus::Active,
124 status_changed_at: now,
125 protocol_info: ProtocolConnectionInfo::empty(),
126 },
127 )
128 .await;
129 let _ = status_tx.send("__UPDATE_UI__".to_string());
130 Log::new(Some(&status_tx))
131 .info(format!("Zabbix sender connected from {}", peer_addr));
132
133 let session = Session {
134 peer_addr,
135 llm_client: llm_client.clone(),
136 app_state: app_state.clone(),
137 status_tx: status_tx.clone(),
138 server_id,
139 protocol: protocol.clone(),
140 connection_id,
141 first_timeout,
142 idle_timeout,
143 };
144 let task_owner = app_state.clone();
146 task_owner
147 .spawn_server_task(server_id, async move {
148 let _permit = permit;
149 session.run(socket).await
150 })
151 .await;
152 }
153 Err(e) => {
154 Log::new(Some(&status_tx)).error(format!("Zabbix accept error: {}", e));
155 break;
156 }
157 }
158 }
159 });
160
161 task_registrar
162 .register_server_task(server_id, accept_handle)
163 .await;
164 Ok(local_addr)
165 }
166}
167
168struct Session {
169 peer_addr: SocketAddr,
170 llm_client: OllamaClient,
171 app_state: Arc<AppState>,
172 status_tx: mpsc::UnboundedSender<String>,
173 server_id: crate::state::ServerId,
174 protocol: Arc<actions::ZabbixProtocol>,
175 connection_id: ConnectionId,
176 first_timeout: Duration,
177 idle_timeout: Duration,
178}
179
180type Writer = Arc<Mutex<tokio::io::WriteHalf<tokio::net::TcpStream>>>;
181
182impl Session {
183 async fn run(self, socket: tokio::net::TcpStream) {
184 let (mut reader, write_half) = tokio::io::split(socket);
185 let writer: Writer = Arc::new(Mutex::new(write_half));
186
187 let peer_rx = crate::server::peer_support::register_peer_channel(
190 &self.app_state,
191 self.server_id,
192 self.connection_id.as_u32(),
193 )
194 .await;
195 crate::server::peer_support::spawn_peer_command_task(
196 peer_rx,
197 self.protocol.clone(),
198 self.app_state.clone(),
199 self.server_id,
200 self.connection_id.as_u32(),
201 writer.clone(),
202 self.status_tx.clone(),
203 );
204
205 self.session(&mut reader, &writer).await;
206
207 self.app_state
208 .remove_peer_handle(self.server_id, self.connection_id.as_u32())
209 .await;
210 let _ = writer.lock().await.shutdown().await;
211 linger(&mut reader).await;
212 self.app_state
213 .update_connection_status(
214 self.server_id,
215 self.connection_id,
216 crate::state::server::ConnectionStatus::Closed,
217 )
218 .await;
219 let _ = self.status_tx.send("__UPDATE_UI__".to_string());
220 }
221
222 async fn write(&self, writer: &Writer, data: &[u8]) {
223 let ok = {
224 let mut w = writer.lock().await;
225 w.write_all(data).await.is_ok() && w.flush().await.is_ok()
226 };
227 if ok {
228 self.app_state
229 .update_connection_stats(
230 self.server_id,
231 self.connection_id,
232 None,
233 Some(data.len() as u64),
234 None,
235 Some(1),
236 )
237 .await;
238 }
239 }
240
241 async fn fill<R: AsyncRead + Unpin>(
244 &self,
245 reader: &mut R,
246 buf: &mut Vec<u8>,
247 want: usize,
248 first: Option<Duration>,
249 ) -> Result<(), &'static str> {
250 let mut chunk = [0u8; 8192];
251 while buf.len() < want {
252 let timeout = match first {
253 Some(t) if buf.is_empty() => t,
254 _ => self.idle_timeout,
255 };
256 let room = (want - buf.len()).min(chunk.len());
257 match tokio::time::timeout(timeout, reader.read(&mut chunk[..room])).await {
258 Err(_) => return Err("timed out"),
259 Ok(Ok(0)) => return Err("closed"),
260 Ok(Ok(n)) => buf.extend_from_slice(&chunk[..n]),
261 Ok(Err(_)) => return Err("read error"),
262 }
263 }
264 Ok(())
265 }
266
267 async fn session<R: AsyncRead + Unpin>(&self, reader: &mut R, writer: &Writer) {
268 let log = Log::new(Some(&self.status_tx));
269 let started = crate::utils::clock::Instant::now();
270 let mut buf: Vec<u8> = Vec::with_capacity(wire::LARGE_HEADER_LEN);
271
272 if let Err(why) = self
274 .fill(reader, &mut buf, 5, Some(self.first_timeout))
275 .await
276 {
277 log.info(format!(
278 "Zabbix peer {} sent no header ({why}); closing",
279 self.peer_addr
280 ));
281 return;
282 }
283 if &buf[..4] != wire::MAGIC {
284 log.warn(format!(
286 "Zabbix peer {} did not send a ZBXD header decision=fail_closed_bad_header",
287 self.peer_addr
288 ));
289 return;
290 }
291 let header_len = wire::header_len(buf[4]);
292 if let Err(why) = self.fill(reader, &mut buf, header_len, None).await {
293 log.info(format!(
294 "Zabbix peer {}: header incomplete ({why}); closing",
295 self.peer_addr
296 ));
297 return;
298 }
299 let header = match wire::parse_header(&buf[..header_len]) {
300 Ok(header) => header,
301 Err(e) => {
302 let (decision, info) = match e {
303 wire::HeaderError::TooLarge(_) => ("fail_closed_too_large", INFO_TOO_LARGE),
304 wire::HeaderError::Compressed => ("fail_closed_compressed", INFO_COMPRESSED),
305 wire::HeaderError::BadFlags(_) | wire::HeaderError::BadMagic => {
306 ("fail_closed_bad_header", INFO_BAD_FLAGS)
307 }
308 };
309 log.warn(format!(
310 "Zabbix peer {}: {:?} decision={} (limit {} bytes)",
311 self.peer_addr,
312 e,
313 decision,
314 wire::MAX_DATA_BYTES
315 ));
316 self.write(writer, &wire::render_failed(info)).await;
317 return;
318 }
319 };
320
321 let total_len = header_len + header.data_len as usize;
323 buf.reserve_exact(total_len - buf.len());
324 if let Err(why) = self.fill(reader, &mut buf, total_len, None).await {
325 log.info(format!(
326 "Zabbix peer {}: request body incomplete ({why}); closing",
327 self.peer_addr
328 ));
329 return;
330 }
331 self.app_state
332 .update_connection_stats(
333 self.server_id,
334 self.connection_id,
335 Some(buf.len() as u64),
336 None,
337 Some(1),
338 None,
339 )
340 .await;
341 let body = &buf[header_len..total_len];
342 log.trace(format!(
343 "Zabbix request from {}: {}",
344 self.peer_addr,
345 String::from_utf8_lossy(body)
346 ));
347
348 let (items, clock) = match wire::parse_request(body) {
349 Ok(wire::Request::SenderData { items, clock }) => (items, clock),
350 Ok(wire::Request::Other(name)) => {
351 log.warn(format!(
352 "Zabbix peer {} sent request {:?} decision=fail_closed_unsupported_request",
353 self.peer_addr, name
354 ));
355 self.write(writer, &wire::render_failed(INFO_UNSUPPORTED))
356 .await;
357 return;
358 }
359 Err(e) => {
360 log.warn(format!(
361 "Zabbix peer {}: {:?} decision=fail_closed_bad_request",
362 self.peer_addr, e
363 ));
364 self.write(writer, &wire::render_failed(e.info())).await;
365 return;
366 }
367 };
368 let total = items.len() as u64;
369 if total == 0 {
370 self.write(
372 writer,
373 &wire::render_result(0, 0, 0, started.elapsed().as_secs_f64()),
374 )
375 .await;
376 return;
377 }
378
379 let reply = self.answer_with_model(&items, clock).await;
380 if let Some((processed, failed)) = reply {
381 self.write(
382 writer,
383 &wire::render_result(processed, failed, total, started.elapsed().as_secs_f64()),
384 )
385 .await;
386 }
387 }
388
389 async fn answer_with_model(
391 &self,
392 items: &[wire::SenderItem],
393 clock: Option<i64>,
394 ) -> Option<(u64, u64)> {
395 let log = Log::new(Some(&self.status_tx));
396 let total = items.len() as u64;
397 let items_json: Vec<serde_json::Value> = items
398 .iter()
399 .map(|i| {
400 let mut v = serde_json::json!({"host": i.host, "key": i.key, "value": i.value});
401 if let Some(c) = i.clock {
402 v["clock"] = c.into();
403 }
404 if let Some(ns) = i.ns {
405 v["ns"] = ns.into();
406 }
407 v
408 })
409 .collect();
410 let mut data = serde_json::json!({
411 "items": items_json,
412 "item_count": total,
413 "answer_with": actions::answer_with_for_items(items.iter().map(|i| i.host.as_str())),
414 });
415 if let Some(c) = clock {
416 data["clock"] = c.into();
417 }
418 let event = Event::new(&actions::ZABBIX_SENDER_DATA_EVENT, data);
419
420 let result = match call_llm(
421 &self.llm_client,
422 &self.app_state,
423 self.server_id,
424 Some(self.connection_id),
425 &event,
426 self.protocol.as_ref(),
427 )
428 .await
429 {
430 Ok(result) => result,
431 Err(e) => {
432 let category = match crate::utils::WireFailure::classify(&e) {
433 crate::utils::WireFailure::Overloaded => "overloaded",
434 crate::utils::WireFailure::Unavailable => "unavailable",
435 };
436 log.warn(format!(
437 "Zabbix {} value(s) from {} decision=fail_closed_llm_error category={}; \
438 answering processed: 0",
439 total, self.peer_addr, category
440 ));
441 log.debug(format!("Zabbix LLM call failed: {}", e));
442 return Some((0, total));
443 }
444 };
445 for message in &result.messages {
446 log.info(message);
447 }
448
449 let mut replies: Vec<Vec<u8>> = Vec::new();
450 let mut close = false;
451 let mut stack = result.protocol_results;
452 stack.reverse();
453 while let Some(item) = stack.pop() {
454 match item {
455 ActionResult::Output(bytes) => replies.push(bytes),
456 ActionResult::CloseConnection => close = true,
457 ActionResult::Multiple(items) => stack.extend(items.into_iter().rev()),
458 _ => {}
459 }
460 }
461
462 let Some(reply) = replies.first() else {
463 if close {
464 log.info(format!(
465 "Zabbix {} value(s) from {} decision=model_close; closing without an answer",
466 total, self.peer_addr
467 ));
468 return None;
469 }
470 log.warn(format!(
471 "Zabbix {} value(s) from {} decision=model_silent ({} failed action(s)); \
472 answering processed: 0",
473 total,
474 self.peer_addr,
475 result.failures.len()
476 ));
477 return Some((0, total));
478 };
479
480 match wire::read_result(reply) {
481 Some((processed, failed, t)) if t == total => {
482 let decision = if processed == 0 {
483 "model_reject"
484 } else {
485 "model_answer"
486 };
487 log.info(format!(
488 "Zabbix {} value(s) from {} decision={} processed={} failed={}",
489 total, self.peer_addr, decision, processed, failed
490 ));
491 Some((processed, failed))
492 }
493 other => {
494 log.warn(format!(
495 "Zabbix {} value(s) from {} decision=fail_closed_mismatched_reply \
496 counts={:?}; answering processed: 0",
497 total, self.peer_addr, other
498 ));
499 Some((0, total))
500 }
501 }
502 }
503}
504
505const LINGER_TIME: Duration = Duration::from_secs(2);
508const LINGER_BYTES: usize = 64 * 1024;
509
510async fn linger<R: AsyncRead + Unpin>(reader: &mut R) {
511 let deadline = tokio::time::Instant::now() + LINGER_TIME;
512 let mut sink = [0u8; 4096];
513 let mut drained = 0usize;
514 while drained < LINGER_BYTES {
515 match tokio::time::timeout_at(deadline, reader.read(&mut sink)).await {
516 Ok(Ok(n)) if n > 0 => drained += n,
517 _ => break,
518 }
519 }
520}