Skip to main content

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}