netget/server/snmp/mod.rs
1//! SNMP agent implementation using rasn-snmp library
2pub mod actions;
3pub mod notify;
4pub mod usm;
5
6use crate::server::connection::ConnectionId;
7use anyhow::Result;
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::net::UdpSocket;
11use tokio::sync::mpsc;
12use tracing::{debug, error, info, trace, warn};
13
14// SNMP protocol support
15use rasn::{ber, types::Integer};
16use rasn_snmp::{v1, v2, v2c};
17
18use crate::llm::action_helper::call_llm;
19use crate::llm::actions::protocol_trait::ActionResult;
20use crate::llm::ollama_client::OllamaClient;
21use crate::protocol::Event;
22use crate::server::SnmpProtocol;
23use crate::state::app_state::AppState;
24use crate::{console_debug, console_trace};
25use actions::SNMP_REQUEST_EVENT;
26
27/// Parsed SNMP message information
28#[derive(Debug)]
29pub struct ParsedSnmpInfo {
30 pub description: String,
31 pub request_type: String,
32 pub version: u8,
33 pub request_id: i32,
34 pub community: Vec<u8>,
35 pub requested_oids: Vec<String>,
36}
37
38/// How an answer is wrapped: in the community message it came in (v1/v2c), or in a USM
39/// message for the user and level it was authenticated at (v3).
40#[derive(Clone)]
41pub enum Framing {
42 Community {
43 version: u8,
44 community: Vec<u8>,
45 },
46 Usm {
47 agent: Arc<usm::Agent>,
48 request: Arc<usm::Request>,
49 },
50}
51
52/// SNMP server that forwards requests to LLM
53/// SNMP `error-status` genErr (RFC 1157 ยง4.1.1, RFC 3416 ยง3): "the agent could not produce
54/// this response for reasons not covered by any other status".
55///
56/// Any non-zero error-status makes the Response PDU an error report rather than an answer, so
57/// no manager can read this as a value for the requested OID.
58pub const SNMP_ERROR_GEN_ERR: u8 = 5;
59/// `error-status` tooBig (RFC 3416 ยง4.2.1): the answer would not fit the manager's msgMaxSize.
60pub const SNMP_ERROR_TOO_BIG: u8 = 1;
61
62/// How deeply a datagram may nest BER constructed values before we refuse to decode it.
63///
64/// A real SNMP message is four levels deep: Message SEQUENCE > PDU > VarBindList > VarBind.
65/// Sixteen leaves generous headroom for anything a manager legitimately sends while keeping
66/// `rasn`'s recursion to a depth no stack cares about.
67pub const MAX_BER_DEPTH: usize = 16;
68
69/// `rasn` 0.18 decodes a *constructed* OCTET STRING by calling
70/// `ber::de::parser::parse_encoded_value` on each of its segments, and a segment may itself be
71/// constructed โ so the function calls itself, with no depth counter anywhere on the path.
72/// `v1::Message` and `v2c::Message` both carry `community` as an OCTET STRING, so the nesting a
73/// datagram declares is the recursion depth we get, and the only bound is the datagram size.
74///
75/// `24 80` (constructed OCTET STRING, indefinite length) is two bytes and buys one level. The
76/// end-of-contents markers can be omitted entirely โ the decoder descends while input remains
77/// and only discovers the missing EOC on the way back up โ so a single 64 KB UDP datagram
78/// reaches roughly 32000 frames. Measured here: 30000 levels aborts the process with
79/// `fatal runtime error: stack overflow`.
80///
81/// That is not a recoverable fault. A Rust stack overflow is a guard-page `SIGSEGV`/`SIGABRT`,
82/// not a panic: `tokio::spawn` cannot isolate it and `catch_unwind` cannot see it, so one
83/// unauthenticated datagram takes down every other server the NetGet process is running.
84///
85/// The screen below therefore walks the TLV structure *iteratively* โ an explicit stack, no
86/// recursion of its own โ and rejects the datagram before `rasn` ever sees it. It also rejects
87/// a definite length that reaches past the end of the datagram, which is the same class of
88/// mistake read from the other side: trust the length the peer declared and you buffer or index
89/// on a number the peer chose.
90///
91/// Returns `Err` with a reason suitable for the log. Never panics, never loops: `pos` strictly
92/// increases on every iteration and every read is bounds-checked.
93pub fn check_ber_structure(data: &[u8], max_depth: usize) -> std::result::Result<(), String> {
94 // `None` = indefinite length, closed by an end-of-contents marker.
95 // `Some(end)` = definite length, closed when `pos` reaches `end`.
96 let mut open: Vec<Option<usize>> = Vec::new();
97 let mut pos = 0usize;
98
99 loop {
100 // Close every definite-length level this position has run past.
101 while let Some(Some(end)) = open.last() {
102 if pos >= *end {
103 open.pop();
104 } else {
105 break;
106 }
107 }
108
109 if pos >= data.len() {
110 break;
111 }
112
113 // An end-of-contents marker closes the innermost indefinite level.
114 if data[pos] == 0x00 && data.get(pos + 1) == Some(&0x00) {
115 if matches!(open.last(), Some(None)) {
116 open.pop();
117 pos += 2;
118 continue;
119 }
120 // A stray 00 00 outside any indefinite level is malformed; let rasn produce the
121 // real diagnostic rather than guessing at it here.
122 return Ok(());
123 }
124
125 // --- identifier octets ---
126 let first = data[pos];
127 let constructed = (first & 0x20) != 0;
128 pos += 1;
129 if first & 0x1f == 0x1f {
130 // High-tag-number form: continuation octets until one without the high bit.
131 loop {
132 let Some(&b) = data.get(pos) else {
133 return Err("truncated BER tag".to_string());
134 };
135 pos += 1;
136 if b & 0x80 == 0 {
137 break;
138 }
139 }
140 }
141
142 // --- length octets ---
143 let Some(&len_first) = data.get(pos) else {
144 return Err("truncated BER length".to_string());
145 };
146 pos += 1;
147
148 let length: Option<usize> = if len_first == 0x80 {
149 None // indefinite
150 } else if len_first & 0x80 == 0 {
151 Some(len_first as usize)
152 } else {
153 let n = (len_first & 0x7f) as usize;
154 // A length-of-length beyond 8 octets cannot describe anything a datagram holds.
155 if n > 8 {
156 return Err(format!("BER length field of {n} octets is not decodable"));
157 }
158 let Some(bytes) = data.get(pos..pos + n) else {
159 return Err("truncated BER long-form length".to_string());
160 };
161 pos += n;
162 let mut v: u64 = 0;
163 for &b in bytes {
164 v = (v << 8) | b as u64;
165 }
166 // Bound the *declared* length against the whole datagram, not against whatever
167 // happens to be left: `usize::try_from` alone would accept 4 GB on a 64-bit box.
168 let v = usize::try_from(v).map_err(|_| "BER length overflows usize".to_string())?;
169 if v > data.len() {
170 return Err(format!(
171 "BER element declares {v} bytes in a {} byte datagram",
172 data.len()
173 ));
174 }
175 Some(v)
176 };
177
178 match (constructed, length) {
179 (true, definite) => {
180 let end = match definite {
181 Some(len) => {
182 let end = pos.checked_add(len).ok_or("BER length overflows")?;
183 if end > data.len() {
184 return Err(format!(
185 "BER element ends at {end}, past the {} byte datagram",
186 data.len()
187 ));
188 }
189 Some(end)
190 }
191 None => None,
192 };
193 open.push(end);
194 if open.len() > max_depth {
195 return Err(format!(
196 "BER nesting deeper than {max_depth} levels; a real SNMP message is 4"
197 ));
198 }
199 // Contents of a constructed value are the next elements: do not skip them.
200 }
201 (false, Some(len)) => {
202 pos = pos.checked_add(len).ok_or("BER length overflows")?;
203 if pos > data.len() {
204 return Err("BER primitive runs past the end of the datagram".to_string());
205 }
206 }
207 (false, None) => {
208 // Indefinite length on a primitive is invalid BER.
209 return Err("indefinite length on a primitive BER value".to_string());
210 }
211 }
212 }
213
214 Ok(())
215}
216
217pub struct SnmpServer;
218
219impl SnmpServer {
220 /// Spawn SNMP agent with integrated LLM actions
221 pub async fn spawn_with_llm_actions(
222 listen_addr: SocketAddr,
223 llm_client: OllamaClient,
224 app_state: Arc<AppState>,
225 status_tx: mpsc::UnboundedSender<String>,
226 server_id: crate::state::ServerId,
227 usm: Option<Arc<usm::Agent>>,
228 ) -> Result<SocketAddr> {
229 let socket = Arc::new(UdpSocket::bind(listen_addr).await?);
230 if let Some(agent) = &usm {
231 info!(
232 "SNMP agent serves SNMPv3 as engine {} (boots {})",
233 hex::encode(&agent.engine_id),
234 agent.boots
235 );
236 }
237 let local_addr = socket.local_addr()?;
238 info!("SNMP agent (action-based) listening on {}", local_addr);
239
240 let protocol = Arc::new(SnmpProtocol::new());
241
242 let task_registrar = app_state.clone();
243 let accept_handle = tokio::spawn(async move {
244 let mut buffer = vec![0u8; 65535];
245
246 loop {
247 match socket.recv_from(&mut buffer).await {
248 Ok((n, peer_addr)) => {
249 let data = buffer[..n].to_vec();
250 let connection_id =
251 ConnectionId::new(app_state.get_next_unified_id().await);
252
253 // Add connection to ServerInstance (SNMP "connection" = recent peer)
254 use crate::state::server::{
255 ConnectionState as ServerConnectionState, ConnectionStatus,
256 ProtocolConnectionInfo,
257 };
258 let now = crate::utils::clock::Instant::now();
259 let conn_state = ServerConnectionState {
260 id: connection_id,
261 remote_addr: peer_addr,
262 local_addr,
263 bytes_sent: 0,
264 bytes_received: n as u64,
265 packets_sent: 0,
266 packets_received: 1,
267 last_activity: now,
268 status: ConnectionStatus::Active,
269 status_changed_at: now,
270 protocol_info: ProtocolConnectionInfo::empty(),
271 };
272 app_state
273 .add_connection_to_server(server_id, conn_state)
274 .await;
275 let _ = status_tx.send("__UPDATE_UI__".to_string());
276
277 // DEBUG: Log summary
278 console_debug!(status_tx, "SNMP received {} bytes from {}", n, peer_addr);
279
280 // TRACE: Log full payload
281 let hex_str = hex::encode(&data);
282 console_trace!(status_tx, "SNMP data (hex): {}", hex_str);
283
284 // SNMPv3: the USM decides first โ discovery, unknown users, wrong
285 // digests and stale clocks are answered with its reports and never
286 // reach the handler.
287 let mut v3_request: Option<Arc<usm::Request>> = None;
288 if check_ber_structure(&data, MAX_BER_DEPTH).is_ok() && usm::is_v3(&data) {
289 let Some(agent) = &usm else {
290 warn!(
291 "SNMP dropping an SNMPv3 message from {} \
292 (decision=dropped_v3_not_configured): no v3_users configured",
293 peer_addr
294 );
295 let _ = status_tx.send(format!(
296 "[WARN] SNMP dropped an SNMPv3 message from {}: no v3_users are configured",
297 peer_addr
298 ));
299 continue;
300 };
301 match agent.receive(&data) {
302 usm::Inbound::Report(bytes, kind) => {
303 info!(
304 "SNMP v3 report {} to {} (decision=usm_report)",
305 kind.name(),
306 peer_addr
307 );
308 let _ = status_tx.send(format!(
309 "[INFO] SNMP v3 {} report to {}",
310 kind.name(),
311 peer_addr
312 ));
313 if let Err(e) = socket.send_to(&bytes, peer_addr).await {
314 error!("SNMP v3 report to {} failed: {}", peer_addr, e);
315 }
316 continue;
317 }
318 usm::Inbound::Drop(reason) => {
319 warn!(
320 "SNMP dropping an SNMPv3 message from {} \
321 (decision=dropped_v3): {}",
322 peer_addr, reason
323 );
324 let _ = status_tx.send(format!(
325 "[WARN] SNMP dropped an SNMPv3 message from {}: {}",
326 peer_addr, reason
327 ));
328 continue;
329 }
330 usm::Inbound::Request(request) => {
331 if !matches!(
332 request.pdu,
333 v2::Pdus::GetRequest(_)
334 | v2::Pdus::GetNextRequest(_)
335 | v2::Pdus::GetBulkRequest(_)
336 | v2::Pdus::SetRequest(_)
337 ) {
338 warn!(
339 "SNMP dropping a v3 {} from {} \
340 (decision=dropped_v3_notification): SNMPv3 \
341 notifications are not served",
342 Self::get_v2_pdu_type(&request.pdu),
343 peer_addr
344 );
345 continue;
346 }
347 v3_request = Some(Arc::from(request));
348 }
349 }
350 }
351
352 // A trap or an inform is a notification, not a request: it has no
353 // variables to answer and goes to the handler as one event.
354 if v3_request.is_none() && check_ber_structure(&data, MAX_BER_DEPTH).is_ok()
355 {
356 if let Some(received) = notify::parse(&data) {
357 let llm = llm_client.clone();
358 let state = app_state.clone();
359 let status = status_tx.clone();
360 let socket = socket.clone();
361 let protocol = protocol.clone();
362 app_state
363 .spawn_server_task(server_id, async move {
364 Self::handle_notification(
365 received, peer_addr, llm, state, status, socket,
366 protocol, server_id,
367 )
368 .await;
369 })
370 .await;
371 continue;
372 }
373 }
374
375 // Parse the SNMP message
376 let parsed = match v3_request
377 .as_ref()
378 .map(|r| Self::describe_v3(r))
379 .map_or_else(|| Self::parse_snmp_message(&data), Ok)
380 {
381 Ok(p) => p,
382 Err(e) => {
383 // Silent on the wire โ there is no request-id to answer, and a
384 // Response PDU without one is not addressable to anything. The
385 // operator still has to be able to see it, so it goes to the
386 // status stream as well as the log.
387 warn!(
388 "SNMP dropping unparseable datagram from {} \
389 (decision=dropped_malformed_request): {}",
390 peer_addr, e
391 );
392 let _ = status_tx.send(format!(
393 "[WARN] SNMP dropped a malformed {} byte datagram from {}: {}",
394 n, peer_addr, e
395 ));
396 continue;
397 }
398 };
399
400 let llm_clone = llm_client.clone();
401 let state_clone = app_state.clone();
402 let status_clone = status_tx.clone();
403 let socket_clone = socket.clone();
404 let protocol_clone = protocol.clone();
405 let request_id = parsed.request_id;
406 let framing = match (&usm, v3_request) {
407 (Some(agent), Some(request)) => Framing::Usm {
408 agent: agent.clone(),
409 request,
410 },
411 _ => Framing::Community {
412 version: parsed.version,
413 community: parsed.community.clone(),
414 },
415 };
416 let requested_oids = parsed.requested_oids.clone();
417
418 // Spawn task to handle request with LLM
419 // Tracked, not detached: stop_server must abort this task too.
420 let task_owner = app_state.clone();
421 task_owner.spawn_server_task(server_id, async move {
422 // Clone for BER encoding later
423 let requested_oids_clone = requested_oids.clone();
424
425 // Create SNMP request event. request_id and the community
426 // string are echoed into the response automatically - they are
427 // exposed here so a handler can inspect or log them.
428 let answer_with = actions::answer_with_for_request(
429 &parsed.request_type,
430 &parsed.requested_oids,
431 );
432 let mut data = serde_json::json!({
433 "request_type": parsed.request_type,
434 "answer_with": answer_with,
435 "oids": parsed.requested_oids,
436 "community": String::from_utf8_lossy(&parsed.community).to_string(),
437 "request_id": parsed.request_id,
438 "version": match parsed.version { 0 => "v1", 3 => "v3", _ => "v2c" },
439 "client_ip": peer_addr.ip().to_string()
440 });
441 if let Framing::Usm { request, .. } = &framing {
442 data["user"] = serde_json::json!(request.user);
443 data["security_level"] = serde_json::json!(request.level.name());
444 data["context_name"] = serde_json::json!(
445 String::from_utf8_lossy(&request.context_name)
446 );
447 }
448 let event = Event::new(&SNMP_REQUEST_EVENT, data);
449
450 debug!("SNMP calling LLM for request from {}", peer_addr);
451 let _ = status_clone.send(format!(
452 "[DEBUG] SNMP calling LLM for request from {}",
453 peer_addr
454 ));
455
456 // Call LLM
457 match call_llm(
458 &llm_clone,
459 &state_clone,
460 server_id,
461 None,
462 &event,
463 protocol_clone.as_ref(),
464 )
465 .await
466 {
467 Ok(execution_result) => {
468 // Display messages from LLM
469 for message in &execution_result.messages {
470 info!("{}", message);
471 let _ = status_clone.send(format!("[INFO] {}", message));
472 }
473
474 debug!(
475 "SNMP got {} protocol results",
476 execution_result.protocol_results.len()
477 );
478 let _ = status_clone.send(format!(
479 "[DEBUG] SNMP got {} protocol results",
480 execution_result.protocol_results.len()
481 ));
482
483 // A model that answers with nothing โ `ignore_request`, or
484 // simply no action at all โ is a real decision, and the
485 // only place it can be recorded is the log: on the wire it
486 // is indistinguishable from the agent being down. Tag it so
487 // it can be told apart from a backend failure, the way
488 // `radius` does.
489 if !execution_result
490 .protocol_results
491 .iter()
492 .any(|r| !r.get_all_output().is_empty())
493 {
494 info!(
495 "SNMP answering nothing to request {} from {} \
496 (decision=model_silent)",
497 request_id, peer_addr
498 );
499 let _ = status_clone.send(format!(
500 "SNMP: no reply to {} (model chose silence)",
501 peer_addr
502 ));
503 }
504
505 // Handle protocol results (send SNMP response)
506 for protocol_result in execution_result.protocol_results {
507 if let Some(output_data) =
508 protocol_result.get_all_output().first()
509 {
510 // Parse JSON response and convert to SNMP BER format
511 let json_str = String::from_utf8_lossy(output_data);
512
513 // An error-status reply is the model refusing this
514 // OID; a value reply is the model answering. Both
515 // are legitimate and neither is a failure, but a
516 // log that cannot tell them apart โ or tell either
517 // from the fail-closed genErr below โ is useless
518 // when something goes wrong.
519 let model_rejected = serde_json::from_str::<
520 serde_json::Value,
521 >(
522 json_str.trim()
523 )
524 .ok()
525 .and_then(|v| v.get("error").and_then(|e| e.as_bool()))
526 .unwrap_or(false);
527 info!(
528 "SNMP answering request {} from {} (decision={})",
529 request_id,
530 peer_addr,
531 if model_rejected {
532 "model_reject"
533 } else {
534 "model_answer"
535 }
536 );
537 match Self::build_snmp_response(
538 &json_str,
539 &framing,
540 request_id,
541 &requested_oids_clone,
542 ) {
543 Ok(snmp_response) => {
544 if let Err(e) = socket_clone
545 .send_to(&snmp_response, peer_addr)
546 .await
547 {
548 error!(
549 "Failed to send SNMP response: {}",
550 e
551 );
552 } else {
553 // DEBUG: Log summary
554 debug!(
555 "SNMP sent {} bytes to {}",
556 snmp_response.len(),
557 peer_addr
558 );
559 let _ = status_clone.send(format!(
560 "[DEBUG] SNMP sent {} bytes to {}",
561 snmp_response.len(),
562 peer_addr
563 ));
564
565 // TRACE: Log full payload
566 let hex_dump: String = snmp_response
567 .iter()
568 .map(|b| format!("{:02X}", b))
569 .collect::<Vec<_>>()
570 .join(" ");
571 trace!("SNMP sent (hex): {}", hex_dump);
572 let _ = status_clone.send(format!(
573 "[TRACE] SNMP sent (hex): {}",
574 hex_dump
575 ));
576
577 let _ = status_clone.send(format!(
578 "โ SNMP response to {} ({} bytes)",
579 peer_addr,
580 snmp_response.len()
581 ));
582 }
583 }
584 Err(e) => {
585 error!(
586 "Failed to build SNMP BER response: {}",
587 e
588 );
589 let _ = status_clone.send(format!(
590 "โ SNMP encoding error: {}",
591 e
592 ));
593 }
594 }
595 }
596 }
597 }
598 Err(e) => {
599 // SNMP is UDP, but unlike bare UDP it carries a
600 // request-id, so a reply is unambiguously *this*
601 // request's answer and cannot be mistaken for anything
602 // else. Silence would leave the manager retrying until
603 // its own timeout and then reporting the agent as down -
604 // which is not what happened.
605 //
606 // genErr(5) is the generic "the agent could not produce
607 // this value" status. It is the only honest choice: the
608 // alternative, a Response PDU with error-status 0, means
609 // the varbinds *are* the answer, so an empty one would
610 // read as "this OID has no value" rather than "nobody
611 // looked". SNMP has no retryable error-status for a GET
612 // (resourceUnavailable(13) is defined for SET), so the
613 // overload distinction lives in the log rather than on
614 // the wire.
615 //
616 // The wire therefore carries one code for both cases; the
617 // `decision=` tag is what separates a backend outage from
618 // the model's own `send_snmp_error`, which logs
619 // `decision=model_reject` above and reaches the wire
620 // through an entirely different path.
621 let overloaded = crate::llm::is_overload_error(&e);
622 let decision = if overloaded {
623 "fail_closed_llm_overload"
624 } else {
625 "fail_closed_llm_error"
626 };
627 error!(
628 "SNMP LLM call failed for request {} from {} (decision={}): {}",
629 request_id, peer_addr, decision, e
630 );
631 let _ = status_clone.send(format!(
632 "[ERROR] SNMP replying genErr to {} for request {} (decision={}): {}",
633 peer_addr, request_id, decision, e
634 ));
635 match Self::build_response_message(
636 &framing,
637 request_id,
638 SNMP_ERROR_GEN_ERR,
639 0,
640 vec![],
641 ) {
642 Ok(pdu) => {
643 if let Err(send_err) =
644 socket_clone.send_to(&pdu, peer_addr).await
645 {
646 error!(
647 "Failed to send SNMP genErr response to {}: {}",
648 peer_addr, send_err
649 );
650 }
651 }
652 Err(build_err) => {
653 error!(
654 "Failed to build SNMP genErr response for {}: {}",
655 peer_addr, build_err
656 );
657 let _ = status_clone.send(format!(
658 "[ERROR] SNMP could not encode genErr for {}: {}",
659 peer_addr, build_err
660 ));
661 }
662 }
663 }
664 }
665 }).await;
666 }
667 Err(e) => {
668 error!("SNMP receive error: {}", e);
669 break;
670 }
671 }
672 }
673 });
674
675 task_registrar
676 .register_server_task(server_id, accept_handle)
677 .await;
678
679 Ok(local_addr)
680 }
681
682 /// Ask the handler about a trap or inform; acknowledge an inform only if it says so.
683 #[allow(clippy::too_many_arguments)]
684 async fn handle_notification(
685 received: notify::Received,
686 peer_addr: SocketAddr,
687 llm_client: OllamaClient,
688 app_state: Arc<AppState>,
689 status_tx: mpsc::UnboundedSender<String>,
690 socket: Arc<UdpSocket>,
691 protocol: Arc<SnmpProtocol>,
692 server_id: crate::state::ServerId,
693 ) {
694 let mut data = received.data;
695 data["client_ip"] = serde_json::json!(peer_addr.ip().to_string());
696 let event = Event::new(&actions::SNMP_NOTIFICATION_EVENT, data);
697 let acknowledged = match call_llm(
698 &llm_client,
699 &app_state,
700 server_id,
701 None,
702 &event,
703 protocol.as_ref(),
704 )
705 .await
706 {
707 Ok(result) => result.protocol_results.iter().any(|r| {
708 matches!(r, ActionResult::Custom { name, .. } if name == "acknowledge_notification")
709 }),
710 Err(e) => {
711 // Not acknowledging is the only safe failure: an inform's sender then retries.
712 warn!(
713 "SNMP {} from {} decision=fail_closed_llm_error: {}",
714 received.kind, peer_addr, e
715 );
716 return;
717 }
718 };
719 let Some(ack) = received.acknowledgement else {
720 info!(
721 "SNMP trap from {} decision={}",
722 peer_addr,
723 if acknowledged {
724 "model_accept"
725 } else {
726 "model_ignore"
727 }
728 );
729 return;
730 };
731 if !acknowledged {
732 info!(
733 "SNMP inform from {} decision=model_ignore: not acknowledged",
734 peer_addr
735 );
736 return;
737 }
738 match socket.send_to(&ack, peer_addr).await {
739 Ok(_) => {
740 info!(
741 "SNMP inform from {} decision=model_accept: acknowledged",
742 peer_addr
743 );
744 let _ = status_tx.send(format!("[INFO] SNMP inform from {peer_addr} acknowledged"));
745 }
746 Err(e) => error!("SNMP inform acknowledgement to {} failed: {}", peer_addr, e),
747 }
748 }
749
750 /// The request summary for an SNMPv3 request the USM has already accepted.
751 fn describe_v3(request: &usm::Request) -> ParsedSnmpInfo {
752 ParsedSnmpInfo {
753 description: format!(
754 "SNMPv3 {} from user {} ({})",
755 Self::get_v2_pdu_type(&request.pdu),
756 request.user,
757 request.level.name()
758 ),
759 request_type: Self::get_v2_pdu_type(&request.pdu),
760 version: 3,
761 request_id: Self::get_v2_request_id(&request.pdu),
762 community: Vec::new(),
763 requested_oids: Self::get_v2_requested_oids(&request.pdu),
764 }
765 }
766
767 /// A Response PDU (tag 0xA2) on its own, for the USM to wrap.
768 fn build_response_pdu(
769 request_id: i32,
770 error_status: u8,
771 error_index: u8,
772 var_binds: Vec<Vec<u8>>,
773 ) -> Vec<u8> {
774 let mut list = vec![0x30];
775 list.extend_from_slice(&Self::encode_length(
776 var_binds.iter().map(|v| v.len()).sum(),
777 ));
778 for v in var_binds {
779 list.extend_from_slice(&v);
780 }
781 let body = [
782 Self::encode_integer(request_id),
783 Self::encode_integer(error_status as i32),
784 Self::encode_integer(error_index as i32),
785 list,
786 ]
787 .concat();
788 let mut pdu = vec![0xA2];
789 pdu.extend_from_slice(&Self::encode_length(body.len()));
790 pdu.extend_from_slice(&body);
791 pdu
792 }
793
794 /// Parse SNMP message and extract relevant information
795 pub fn parse_snmp_message(data: &[u8]) -> Result<ParsedSnmpInfo> {
796 // Screen the TLV structure before rasn sees it. Its BER decoder recurses once per
797 // level of constructed nesting with no bound, and a stack overflow is fatal to the
798 // whole process rather than to this datagram. See `check_ber_structure`.
799 if let Err(reason) = check_ber_structure(data, MAX_BER_DEPTH) {
800 return Err(anyhow::anyhow!(
801 "Rejected malformed SNMP message ({} bytes): {}",
802 data.len(),
803 reason
804 ));
805 }
806
807 // Try to decode as SNMPv2c first (most common)
808 if let Ok(msg) = ber::decode::<v2c::Message<v2::Pdus>>(data) {
809 let request_type = Self::get_v2_pdu_type(&msg.data);
810 let request_id = Self::get_v2_request_id(&msg.data);
811 let requested_oids = Self::get_v2_requested_oids(&msg.data);
812 return Ok(ParsedSnmpInfo {
813 description: Self::format_v2c_message(&msg),
814 request_type,
815 version: 1, // v2c uses version 1 in the packet
816 request_id,
817 community: msg.community.to_vec(),
818 requested_oids,
819 });
820 }
821
822 // Try SNMPv1
823 if let Ok(msg) = ber::decode::<v1::Message<v1::Pdus>>(data) {
824 let request_type = Self::get_v1_pdu_type(&msg.data);
825 let request_id = Self::get_v1_request_id(&msg.data);
826 let requested_oids = Self::get_v1_requested_oids(&msg.data);
827 return Ok(ParsedSnmpInfo {
828 description: Self::format_v1_message(&msg),
829 request_type,
830 version: 0,
831 request_id,
832 community: msg.community.to_vec(),
833 requested_oids,
834 });
835 }
836
837 // If we can't parse it, return error
838 Err(anyhow::anyhow!(
839 "Failed to parse SNMP message: {} bytes",
840 data.len()
841 ))
842 }
843
844 /// Get request ID for v2
845 fn get_v2_request_id(pdu: &v2::Pdus) -> i32 {
846 match pdu {
847 v2::Pdus::GetRequest(p) => p.0.request_id,
848 v2::Pdus::GetNextRequest(p) => p.0.request_id,
849 v2::Pdus::GetBulkRequest(p) => p.0.request_id,
850 v2::Pdus::SetRequest(p) => p.0.request_id,
851 v2::Pdus::Response(p) => p.0.request_id,
852 v2::Pdus::InformRequest(p) => p.0.request_id,
853 v2::Pdus::Trap(p) => p.0.request_id,
854 v2::Pdus::Report(p) => p.0.request_id,
855 }
856 }
857
858 /// Get requested OIDs for v2
859 fn get_v2_requested_oids(pdu: &v2::Pdus) -> Vec<String> {
860 let bindings = match pdu {
861 v2::Pdus::GetRequest(p) => &p.0.variable_bindings,
862 v2::Pdus::GetNextRequest(p) => &p.0.variable_bindings,
863 v2::Pdus::GetBulkRequest(p) => &p.0.variable_bindings,
864 v2::Pdus::SetRequest(p) => &p.0.variable_bindings,
865 _ => return vec![],
866 };
867
868 bindings.iter().map(|vb| vb.name.to_string()).collect()
869 }
870
871 /// Get request ID for v1
872 fn get_v1_request_id(pdu: &v1::Pdus) -> i32 {
873 let integer = match pdu {
874 v1::Pdus::GetRequest(p) => &p.0.request_id,
875 v1::Pdus::GetNextRequest(p) => &p.0.request_id,
876 v1::Pdus::GetResponse(p) => &p.0.request_id,
877 v1::Pdus::SetRequest(p) => &p.0.request_id,
878 _ => return 0,
879 };
880
881 // Convert Integer to i32
882 match integer {
883 Integer::Primitive(val) => *val as i32,
884 Integer::Variable(big) => {
885 // Try to convert BigInt to i32, default to 0 if out of range
886 big.to_string().parse::<i32>().unwrap_or(0)
887 }
888 }
889 }
890
891 /// Get requested OIDs for v1
892 fn get_v1_requested_oids(pdu: &v1::Pdus) -> Vec<String> {
893 let bindings = match pdu {
894 v1::Pdus::GetRequest(p) => &p.0.variable_bindings,
895 v1::Pdus::GetNextRequest(p) => &p.0.variable_bindings,
896 v1::Pdus::SetRequest(p) => &p.0.variable_bindings,
897 _ => return vec![],
898 };
899
900 bindings.iter().map(|vb| vb.name.to_string()).collect()
901 }
902
903 /// Get PDU type for v2
904 fn get_v2_pdu_type(pdu: &v2::Pdus) -> String {
905 match pdu {
906 v2::Pdus::GetRequest(_) => "GetRequest",
907 v2::Pdus::GetNextRequest(_) => "GetNextRequest",
908 v2::Pdus::GetBulkRequest(_) => "GetBulkRequest",
909 v2::Pdus::SetRequest(_) => "SetRequest",
910 v2::Pdus::Response(_) => "Response",
911 v2::Pdus::InformRequest(_) => "InformRequest",
912 v2::Pdus::Trap(_) => "Trap",
913 v2::Pdus::Report(_) => "Report",
914 }
915 .to_string()
916 }
917
918 /// Get PDU type for v1
919 fn get_v1_pdu_type(pdu: &v1::Pdus) -> String {
920 match pdu {
921 v1::Pdus::GetRequest(_) => "GetRequest",
922 v1::Pdus::GetNextRequest(_) => "GetNextRequest",
923 v1::Pdus::GetResponse(_) => "GetResponse",
924 v1::Pdus::SetRequest(_) => "SetRequest",
925 v1::Pdus::Trap(_) => "Trap",
926 }
927 .to_string()
928 }
929
930 /// Format SNMPv2c message with OIDs
931 fn format_v2c_message(msg: &v2c::Message<v2::Pdus>) -> String {
932 let mut info = "SNMPv2c Message:\n".to_string();
933 info.push_str(&format!(
934 " Community: {}\n",
935 String::from_utf8_lossy(&msg.community)
936 ));
937
938 match &msg.data {
939 v2::Pdus::GetRequest(pdu) => {
940 info.push_str(" Type: GetRequest\n");
941 info.push_str(&format!(" Request ID: {}\n", pdu.0.request_id));
942 info.push_str(&Self::format_v2_var_binds(&pdu.0.variable_bindings));
943 }
944 v2::Pdus::GetNextRequest(pdu) => {
945 info.push_str(" Type: GetNextRequest\n");
946 info.push_str(&format!(" Request ID: {}\n", pdu.0.request_id));
947 info.push_str(&Self::format_v2_var_binds(&pdu.0.variable_bindings));
948 }
949 v2::Pdus::GetBulkRequest(pdu) => {
950 info.push_str(" Type: GetBulkRequest\n");
951 info.push_str(&format!(" Request ID: {}\n", pdu.0.request_id));
952 info.push_str(&format!(" Non-repeaters: {}\n", pdu.0.non_repeaters));
953 info.push_str(&format!(" Max-repetitions: {}\n", pdu.0.max_repetitions));
954 info.push_str(&Self::format_v2_var_binds(&pdu.0.variable_bindings));
955 }
956 _ => {
957 info.push_str(&format!(" Type: {}\n", Self::get_v2_pdu_type(&msg.data)));
958 }
959 }
960
961 info
962 }
963
964 /// Format SNMPv1 message with OIDs
965 fn format_v1_message(msg: &v1::Message<v1::Pdus>) -> String {
966 let mut info = "SNMPv1 Message:\n".to_string();
967 info.push_str(&format!(
968 " Community: {}\n",
969 String::from_utf8_lossy(&msg.community)
970 ));
971
972 match &msg.data {
973 v1::Pdus::GetRequest(pdu) => {
974 info.push_str(" Type: GetRequest\n");
975 info.push_str(&format!(" Request ID: {}\n", pdu.0.request_id));
976 info.push_str(&Self::format_v1_var_binds(&pdu.0.variable_bindings));
977 }
978 v1::Pdus::GetNextRequest(pdu) => {
979 info.push_str(" Type: GetNextRequest\n");
980 info.push_str(&format!(" Request ID: {}\n", pdu.0.request_id));
981 info.push_str(&Self::format_v1_var_binds(&pdu.0.variable_bindings));
982 }
983 _ => {
984 info.push_str(&format!(" Type: {}\n", Self::get_v1_pdu_type(&msg.data)));
985 }
986 }
987
988 info
989 }
990
991 /// Format v2 variable bindings
992 fn format_v2_var_binds(bindings: &[v2::VarBind]) -> String {
993 let mut result = String::from(" Requested OIDs:\n");
994 if bindings.is_empty() {
995 result.push_str(" (none - requesting all)\n");
996 } else {
997 for (i, bind) in bindings.iter().enumerate() {
998 result.push_str(&format!(" [{}] {}\n", i + 1, bind.name));
999 }
1000 }
1001 result
1002 }
1003
1004 /// Format v1 variable bindings
1005 fn format_v1_var_binds(bindings: &[v1::VarBind]) -> String {
1006 let mut result = String::from(" Requested OIDs:\n");
1007 if bindings.is_empty() {
1008 result.push_str(" (none - requesting all)\n");
1009 } else {
1010 for (i, bind) in bindings.iter().enumerate() {
1011 result.push_str(&format!(" [{}] {}\n", i + 1, bind.name));
1012 }
1013 }
1014 result
1015 }
1016
1017 /// Build SNMP response from LLM output using manual BER encoding
1018 pub fn build_snmp_response(
1019 llm_response: &str,
1020 framing: &Framing,
1021 request_id: i32,
1022 requested_oids: &[String],
1023 ) -> Result<Vec<u8>> {
1024 let trimmed = llm_response.trim();
1025
1026 // Try to parse as JSON first
1027 if let Ok(response_data) = serde_json::from_str::<serde_json::Value>(trimmed) {
1028 if !response_data.is_object() {
1029 // Not an object, fall through to plain text handling
1030 } else if response_data.get("variables").is_some()
1031 || response_data.get("error").is_some()
1032 {
1033 // SNMP-specific format with variables
1034 // Check for error
1035 if response_data["error"].as_bool().unwrap_or(false) {
1036 let error_msg = response_data["error_message"]
1037 .as_str()
1038 .unwrap_or("Unknown error");
1039 // SNMP carries the reason as a numeric error-status; the text is for
1040 // our logs only, since the wire format has nowhere to put it.
1041 let error_status = response_data["error_status"].as_u64().unwrap_or(5) as u8;
1042 let error_index = response_data["error_index"].as_u64().unwrap_or(0) as u8;
1043 debug!(
1044 "LLM reported SNMP error status {}: {}",
1045 error_status, error_msg
1046 );
1047 return Self::build_response_message(
1048 framing,
1049 request_id,
1050 error_status,
1051 error_index,
1052 vec![],
1053 );
1054 }
1055
1056 // Build response with variable bindings
1057 let mut var_binds = Vec::new();
1058
1059 if let Some(variables) = response_data["variables"].as_array() {
1060 for var in variables {
1061 let oid_str = var["oid"].as_str().unwrap_or("");
1062 let value_type = var["type"].as_str().unwrap_or("null");
1063 let value = &var["value"];
1064
1065 // DEBUG: Log the actual value being returned
1066 let value_str = match value_type {
1067 "string" => format!("\"{}\"", value.as_str().unwrap_or("")),
1068 "integer" => format!("{}", value.as_i64().unwrap_or(0)),
1069 "counter" | "gauge" | "timeticks" => {
1070 format!("{}", value.as_u64().unwrap_or(0))
1071 }
1072 "null" | _ => "null".to_string(),
1073 };
1074 debug!(
1075 "SNMP response: {} = {} ({})",
1076 oid_str, value_str, value_type
1077 );
1078
1079 // Encode each variable binding
1080 let var_bind = Self::encode_var_bind(oid_str, value_type, value)?;
1081 var_binds.push(var_bind);
1082 }
1083 }
1084
1085 // Build the complete SNMP response message
1086 return Self::build_response_message(framing, request_id, 0, 0, var_binds);
1087 } else if let Some(output) = response_data.get("output") {
1088 // Standard LlmResponse format - extract output field and process as text
1089 if let Some(output_str) = output.as_str() {
1090 debug!(
1091 "LLM returned LlmResponse format, using 'output' field: {}",
1092 output_str
1093 );
1094
1095 // Use the first requested OID
1096 let oid = requested_oids
1097 .first()
1098 .map(|s| s.as_str())
1099 .unwrap_or("1.3.6.1.2.1.1.1.0");
1100
1101 // Try to parse as integer first
1102 let var_bind = if let Ok(num) = output_str.trim().parse::<i32>() {
1103 debug!("SNMP response: {} = {} (integer)", oid, num);
1104 Self::encode_var_bind(oid, "integer", &serde_json::Value::from(num))?
1105 } else {
1106 // Treat as string
1107 debug!("SNMP response: {} = \"{}\" (string)", oid, output_str);
1108 Self::encode_var_bind(oid, "string", &serde_json::Value::from(output_str))?
1109 };
1110
1111 return Self::build_response_message(framing, request_id, 0, 0, vec![var_bind]);
1112 }
1113 // If output is null or not a string, fall through to plain text handling
1114 }
1115 // If JSON parsed but not recognized format, fall through to plain text handling
1116 }
1117
1118 // Fallback: treat as plain text value
1119 // Use the first requested OID if available, otherwise use a default
1120 let oid = requested_oids
1121 .first()
1122 .map(|s| s.as_str())
1123 .unwrap_or("1.3.6.1.2.1.1.1.0");
1124
1125 debug!(
1126 "LLM returned plain text (not JSON), treating as simple value for OID {}: {}",
1127 oid, trimmed
1128 );
1129
1130 // Try to parse as integer first
1131 let var_bind = if let Ok(num) = trimmed.parse::<i32>() {
1132 debug!("SNMP response: {} = {} (integer)", oid, num);
1133 Self::encode_var_bind(oid, "integer", &serde_json::Value::from(num))?
1134 } else {
1135 // Treat as string
1136 debug!("SNMP response: {} = \"{}\" (string)", oid, trimmed);
1137 Self::encode_var_bind(oid, "string", &serde_json::Value::from(trimmed))?
1138 };
1139
1140 Self::build_response_message(framing, request_id, 0, 0, vec![var_bind])
1141 }
1142
1143 /// Encode a BER definite length.
1144 ///
1145 /// Short form for < 128, then one, two or three length bytes. The previous code only
1146 /// handled the short form and `0x81` with a `len as u8`, so any value 256 bytes or
1147 /// longer - a long sysDescr, or simply a response with several variable bindings -
1148 /// silently wrapped to a wrong length and produced a packet the client rejected.
1149 fn encode_length(len: usize) -> Vec<u8> {
1150 if len < 0x80 {
1151 vec![len as u8]
1152 } else if len <= 0xFF {
1153 vec![0x81, len as u8]
1154 } else if len <= 0xFFFF {
1155 vec![0x82, (len >> 8) as u8, len as u8]
1156 } else {
1157 vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
1158 }
1159 }
1160
1161 /// Encode a single variable binding
1162 fn encode_var_bind(
1163 oid_str: &str,
1164 value_type: &str,
1165 value: &serde_json::Value,
1166 ) -> Result<Vec<u8>> {
1167 let mut result = Vec::new();
1168
1169 // Encode OID
1170 let oid_bytes = Self::encode_oid(oid_str)?;
1171
1172 // Encode value based on type
1173 let value_bytes = match value_type {
1174 "string" => {
1175 let s = value.as_str().unwrap_or("");
1176 Self::encode_octet_string(s.as_bytes())
1177 }
1178 "integer" => {
1179 let n = value.as_i64().unwrap_or(0) as i32;
1180 Self::encode_integer(n)
1181 }
1182 "counter" => {
1183 let n = value.as_u64().unwrap_or(0) as u32;
1184 Self::encode_counter(n)
1185 }
1186 "gauge" => {
1187 let n = value.as_u64().unwrap_or(0) as u32;
1188 Self::encode_gauge(n)
1189 }
1190 "timeticks" => {
1191 let n = value.as_u64().unwrap_or(0) as u32;
1192 Self::encode_timeticks(n)
1193 }
1194 "null" | _ => {
1195 vec![0x05, 0x00] // NULL
1196 }
1197 };
1198
1199 // Construct SEQUENCE for variable binding
1200 result.push(0x30); // SEQUENCE tag
1201 let len = oid_bytes.len() + value_bytes.len();
1202 result.extend_from_slice(&Self::encode_length(len));
1203 result.extend_from_slice(&oid_bytes);
1204 result.extend_from_slice(&value_bytes);
1205
1206 Ok(result)
1207 }
1208
1209 /// Encode OID
1210 fn encode_oid(oid_str: &str) -> Result<Vec<u8>> {
1211 // Every component must be a number. Skipping unparseable ones (the previous
1212 // behaviour) turned a typo into a different, silently wrong OID.
1213 let mut parts: Vec<u32> = Vec::new();
1214 for component in oid_str.split('.').filter(|s| !s.is_empty()) {
1215 parts.push(component.parse().map_err(|_| {
1216 anyhow::anyhow!(
1217 "Invalid OID {oid_str:?}: component {component:?} is not a number. \
1218 Use dotted decimal, e.g. \"1.3.6.1.2.1.1.1.0\""
1219 )
1220 })?);
1221 }
1222
1223 if parts.len() < 2 {
1224 return Err(anyhow::anyhow!(
1225 "Invalid OID {oid_str:?}: at least two components are required, \
1226 e.g. \"1.3.6.1.2.1.1.1.0\""
1227 ));
1228 }
1229
1230 // First arc is 0, 1 or 2; the second is below 40 unless the first is 2.
1231 if parts[0] > 2 {
1232 return Err(anyhow::anyhow!(
1233 "Invalid OID {oid_str:?}: the first component must be 0, 1 or 2, got {}",
1234 parts[0]
1235 ));
1236 }
1237 if parts[0] < 2 && parts[1] >= 40 {
1238 return Err(anyhow::anyhow!(
1239 "Invalid OID {oid_str:?}: with a first component of {}, the second must be \
1240 below 40, got {}",
1241 parts[0],
1242 parts[1]
1243 ));
1244 }
1245
1246 let mut encoded = Vec::new();
1247
1248 // First two components share one base-128 value: first * 40 + second.
1249 let first_arc = parts[0] * 40 + parts[1];
1250 Self::push_base128(&mut encoded, first_arc);
1251
1252 // Encode remaining components
1253 for &part in &parts[2..] {
1254 Self::push_base128(&mut encoded, part);
1255 }
1256
1257 // Wrap with OID tag
1258 let mut result = vec![0x06]; // OBJECT IDENTIFIER tag
1259 result.extend_from_slice(&Self::encode_length(encoded.len()));
1260 result.extend_from_slice(&encoded);
1261
1262 Ok(result)
1263 }
1264
1265 /// Append one OID sub-identifier in base-128, high bit set on all but the last byte.
1266 fn push_base128(out: &mut Vec<u8>, value: u32) {
1267 if value < 128 {
1268 out.push(value as u8);
1269 return;
1270 }
1271
1272 let mut bytes = Vec::new();
1273 let mut val = value;
1274 while val > 0 {
1275 bytes.push((val & 0x7F) as u8);
1276 val >>= 7;
1277 }
1278 bytes.reverse();
1279
1280 let last = bytes.len() - 1;
1281 for (i, &byte) in bytes.iter().enumerate() {
1282 if i < last {
1283 out.push(byte | 0x80);
1284 } else {
1285 out.push(byte);
1286 }
1287 }
1288 }
1289
1290 /// Encode integer
1291 fn encode_integer(value: i32) -> Vec<u8> {
1292 let bytes = value.to_be_bytes();
1293 let mut result = vec![0x02]; // INTEGER tag
1294
1295 // Skip leading zeros/ones for minimal encoding
1296 let mut start = 0;
1297 if value >= 0 {
1298 while start < 3 && bytes[start] == 0 && (bytes[start + 1] & 0x80) == 0 {
1299 start += 1;
1300 }
1301 } else {
1302 while start < 3 && bytes[start] == 0xFF && (bytes[start + 1] & 0x80) != 0 {
1303 start += 1;
1304 }
1305 }
1306
1307 let len = 4 - start;
1308 result.push(len as u8);
1309 result.extend_from_slice(&bytes[start..]);
1310
1311 result
1312 }
1313
1314 /// Encode octet string
1315 fn encode_octet_string(value: &[u8]) -> Vec<u8> {
1316 let mut result = vec![0x04]; // OCTET STRING tag
1317 result.extend_from_slice(&Self::encode_length(value.len()));
1318 result.extend_from_slice(value);
1319 result
1320 }
1321
1322 /// Encode counter (application tag 1)
1323 fn encode_counter(value: u32) -> Vec<u8> {
1324 let bytes = value.to_be_bytes();
1325 let mut result = vec![0x41]; // Counter tag (application class, tag 1)
1326
1327 // Skip leading zeros
1328 let mut start = 0;
1329 while start < 3 && bytes[start] == 0 {
1330 start += 1;
1331 }
1332
1333 let len = 4 - start;
1334 result.push(len as u8);
1335 result.extend_from_slice(&bytes[start..]);
1336
1337 result
1338 }
1339
1340 /// Encode gauge (application tag 2)
1341 fn encode_gauge(value: u32) -> Vec<u8> {
1342 let bytes = value.to_be_bytes();
1343 let mut result = vec![0x42]; // Gauge tag (application class, tag 2)
1344
1345 // Skip leading zeros
1346 let mut start = 0;
1347 while start < 3 && bytes[start] == 0 {
1348 start += 1;
1349 }
1350
1351 let len = 4 - start;
1352 result.push(len as u8);
1353 result.extend_from_slice(&bytes[start..]);
1354
1355 result
1356 }
1357
1358 /// Encode timeticks (application tag 3)
1359 fn encode_timeticks(value: u32) -> Vec<u8> {
1360 let bytes = value.to_be_bytes();
1361 let mut result = vec![0x43]; // TimeTicks tag (application class, tag 3)
1362
1363 // Skip leading zeros
1364 let mut start = 0;
1365 while start < 3 && bytes[start] == 0 {
1366 start += 1;
1367 }
1368
1369 let len = 4 - start;
1370 result.push(len as u8);
1371 result.extend_from_slice(&bytes[start..]);
1372
1373 result
1374 }
1375
1376 /// Build complete SNMP response message
1377 fn build_response_message(
1378 framing: &Framing,
1379 request_id: i32,
1380 error_status: u8,
1381 error_index: u8,
1382 var_binds: Vec<Vec<u8>>,
1383 ) -> Result<Vec<u8>> {
1384 let (version, community) = match framing {
1385 Framing::Community { version, community } => (*version, community.as_slice()),
1386 Framing::Usm { agent, request } => {
1387 let pdu =
1388 Self::build_response_pdu(request_id, error_status, error_index, var_binds);
1389 let pdus: v2::Pdus = ber::decode(&pdu)
1390 .map_err(|e| anyhow::anyhow!("response PDU does not decode: {e}"))?;
1391 let message = agent.respond(request, pdus)?;
1392 // RFC 3416 ยง4.2.1: an answer larger than the manager can take is tooBig, with
1393 // no variable bindings.
1394 if message.len() as i64 > request.max_size {
1395 let pdu = Self::build_response_pdu(request_id, SNMP_ERROR_TOO_BIG, 0, vec![]);
1396 let pdus: v2::Pdus = ber::decode(&pdu)
1397 .map_err(|e| anyhow::anyhow!("tooBig PDU does not decode: {e}"))?;
1398 return agent.respond(request, pdus);
1399 }
1400 return Ok(message);
1401 }
1402 };
1403 let mut message = Vec::new();
1404
1405 // Encode version
1406 let version_bytes = Self::encode_integer(version as i32);
1407
1408 // Encode community
1409 let community_bytes = Self::encode_octet_string(community);
1410
1411 // Build GetResponse PDU (tag 0xA2)
1412 let mut pdu = vec![0xA2]; // GetResponse tag (context-specific, constructed, tag 2)
1413
1414 // Encode request ID
1415 let request_id_bytes = Self::encode_integer(request_id);
1416
1417 // Encode error status
1418 let error_status_bytes = Self::encode_integer(error_status as i32);
1419
1420 // Encode error index
1421 let error_index_bytes = Self::encode_integer(error_index as i32);
1422
1423 // Encode variable bindings list
1424 let mut var_binds_list = vec![0x30]; // SEQUENCE tag
1425 let var_binds_total_len: usize = var_binds.iter().map(|v| v.len()).sum();
1426 var_binds_list.extend_from_slice(&Self::encode_length(var_binds_total_len));
1427
1428 for var_bind in var_binds {
1429 var_binds_list.extend_from_slice(&var_bind);
1430 }
1431
1432 // Calculate PDU length
1433 let pdu_len = request_id_bytes.len()
1434 + error_status_bytes.len()
1435 + error_index_bytes.len()
1436 + var_binds_list.len();
1437 pdu.extend_from_slice(&Self::encode_length(pdu_len));
1438
1439 pdu.extend_from_slice(&request_id_bytes);
1440 pdu.extend_from_slice(&error_status_bytes);
1441 pdu.extend_from_slice(&error_index_bytes);
1442 pdu.extend_from_slice(&var_binds_list);
1443
1444 // Build complete message
1445 message.push(0x30); // SEQUENCE tag
1446 let message_len = version_bytes.len() + community_bytes.len() + pdu.len();
1447 message.extend_from_slice(&Self::encode_length(message_len));
1448
1449 message.extend_from_slice(&version_bytes);
1450 message.extend_from_slice(&community_bytes);
1451 message.extend_from_slice(&pdu);
1452
1453 Ok(message)
1454 }
1455}