1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub struct SnmpProtocol;
16
17impl SnmpProtocol {
18 pub fn new() -> Self {
19 Self
20 }
21}
22
23impl Protocol for SnmpProtocol {
25 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
26 vec![send_trap_action()]
28 }
29 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
30 vec![
31 send_snmp_response_action(),
32 send_snmp_error_action(),
33 ignore_request_action(),
34 acknowledge_notification_action(),
35 ignore_notification_action(),
36 ]
37 }
38 fn protocol_name(&self) -> &'static str {
39 "SNMP"
40 }
41 fn get_event_types(&self) -> Vec<EventType> {
42 get_snmp_event_types()
43 }
44 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
45 use crate::llm::actions::ParameterDefinition;
46 vec![
47 ParameterDefinition {
48 name: "v3_users".into(),
49 type_hint: "array".into(),
50 description: "SNMPv3 USM users: [{name, auth_protocol: none|md5|sha|sha224|sha256|sha384|sha512, auth_password, priv_protocol: none|des|aes, priv_password}]. Passwords are 8 to 256 bytes. A user is answered only at the level it is configured for (authPriv users never get an unencrypted answer); others get the USM's reports. Absent means SNMPv3 is not served and v3 messages are dropped".into(),
51 required: false,
52 example: json!([{"name": "monitor", "auth_protocol": "sha256", "auth_password": "monitor-auth-pass", "priv_protocol": "aes", "priv_password": "monitor-priv-pass"}]),
53 default: None,
54 },
55 ParameterDefinition {
56 name: "engine_id".into(),
57 type_hint: "string".into(),
58 description: "The agent's snmpEngineID in hexadecimal, 5 to 32 bytes, as Net-SNMP writes it. Absent means a random RFC 3411 format-5 ID chosen at start".into(),
59 required: false,
60 example: json!("8000000005a1b2c3d4e5f60718"),
61 default: None,
62 },
63 ParameterDefinition {
64 name: "engine_boots".into(),
65 type_hint: "number".into(),
66 description: "The agent's snmpEngineBoots, 1 to 2147483646. Absent means the number of wall-clock seconds since 2024-01-01, so each restart reports more boots than the last and an authenticated message captured in one run is refused in the next even with a fixed engine_id. Set it only where the wall clock cannot be trusted, and then raise it on every restart".into(),
67 required: false,
68 example: json!(2),
69 default: None,
72 },
73 ]
74 }
75 fn stack_name(&self) -> &'static str {
76 "ETH>IP>UDP>SNMP"
77 }
78 fn keywords(&self) -> Vec<&'static str> {
79 vec!["snmp", "snmp agent"]
80 }
81 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
82 use crate::protocol::metadata::{
83 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
84 };
85
86 ProtocolMetadataV2::builder()
87 .connectionless()
88 .answers_on_failure()
90 .state(DevelopmentState::Beta)
91 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(161))
92 .well_known_udp_port(161)
93 .implementation("rasn-snmp v0.18 for parsing + manual BER encoding; SNMPv3 USM in usm.rs (RustCrypto hmac/md-5/sha1/sha2/des/aes)")
94 .llm_control("OID responses (sysDescr, ifTable, custom MIBs)")
95 .e2e_testing("Net-SNMP's own snmpget/snmpgetnext (tests/server/snmp/test.rs), run with -On -Oe so the assertions pin the decoded type tag and value rather than the local MIB set. Not ignored, and it hard-fails rather than skipping when the binaries are absent. Not the `snmp` Rust crate, whatever the older notes said. Notifications (tests/server/snmp/notification_test.rs): Net-SNMP's snmptrap (v1 and v2c) and snmpinform send to NetGet, and snmptrapd receives NetGet's v2c trap, v2c inform (acknowledged) and v1 trap. SNMPv3 (tests/server/snmp/usm_test.rs): Net-SNMP's snmpget/getnext/bulkget/set and pysnmp 7.1.22 at every level, with MD5, SHA-1, SHA-224/256/384/512 and DES/AES-128, plus the USM's refusals and RFC 3414's key vectors")
96 .notes("SNMPv1/v2c with a community; SNMPv3 with the USM when v3_users is set (RFC 3414 procedure and reports, HMAC-MD5/SHA-1/SHA-2 authentication, DES-CBC and AES-128-CFB privacy; a user is served only at its configured level; no VACM - the event names the user and level - no AES-192/256, no v3 notifications). request-id and community are echoed from the request automatically. Notifications both ways: send_trap sends v1/v2c traps and v2c informs (resent until acknowledged), and received traps and informs are snmp_notification events, an inform acknowledged only when the handler says so. Incoming BER is depth- and length-screened before rasn decodes it, decrypted v3 PDUs included: rasn 0.18 recurses without a bound on constructed OCTET STRINGs, and a 60 KB datagram was enough to stack-overflow the whole process")
97 .build()
98 }
99 fn description(&self) -> &'static str {
100 "SNMP agent for network monitoring"
101 }
102 fn example_prompt(&self) -> &'static str {
103 "SNMP Port 8161 serve OID 1.3.6.1.2.1.1.1.0 (sysDescr) return 'NetGet SNMP Server v0.1'"
104 }
105 fn group_name(&self) -> &'static str {
106 "Core"
107 }
108 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
109 use crate::llm::actions::StartupExamples;
110 StartupExamples::new(
111 json!({
113 "type": "open_server",
114 "port": 161,
115 "base_stack": "snmp",
116 "instruction": "SNMP agent serving OID 1.3.6.1.2.1.1.1.0 (sysDescr) as 'NetGet SNMP Server v1.0'"
117 }),
118 json!({
120 "type": "open_server",
121 "port": 161,
122 "base_stack": "snmp",
123 "event_handlers": [{
124 "event_pattern": "snmp_request",
125 "handler": {
126 "type": "script",
127 "language": "python",
128 "code": "# Handle SNMP GET request\noids = event.get('oids', [])\nvariables = [{'oid': oid, 'type': 'string', 'value': 'NetGet SNMP Server'} for oid in oids]\nrespond([{'type': 'send_snmp_response', 'variables': variables}])"
129 }
130 }]
131 }),
132 json!({
134 "type": "open_server",
135 "port": 161,
136 "base_stack": "snmp",
137 "event_handlers": [{
138 "event_pattern": "snmp_request",
139 "handler": {
140 "type": "static",
141 "actions": [{
142 "type": "send_snmp_response",
143 "variables": [{
144 "oid": "1.3.6.1.2.1.1.1.0",
145 "type": "string",
146 "value": "NetGet SNMP Server v1.0"
147 }]
148 }]
149 }
150 }]
151 }),
152 )
153 }
154}
155
156impl Server for SnmpProtocol {
158 fn spawn(
159 &self,
160 ctx: crate::protocol::SpawnContext,
161 ) -> std::pin::Pin<
162 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
163 > {
164 Box::pin(async move {
165 use crate::server::snmp::SnmpServer;
166 let usm = super::usm::agent_from_params(ctx.startup_params.as_ref())?;
167 SnmpServer::spawn_with_llm_actions(
168 ctx.legacy_listen_addr(),
169 ctx.llm_client,
170 ctx.state,
171 ctx.status_tx,
172 ctx.server_id,
173 usm,
174 )
175 .await
176 })
177 }
178 fn execute_action_with_state<'a>(
179 &'a self,
180 action: serde_json::Value,
181 state: AppState,
182 server_id: Option<crate::state::ServerId>,
183 ) -> std::pin::Pin<
184 Box<dyn std::future::Future<Output = anyhow::Result<ActionResult>> + Send + 'a>,
185 > {
186 if action.get("type").and_then(|v| v.as_str()) != Some("send_trap") {
187 let result = self.execute_action(action);
188 return Box::pin(async move { result });
189 }
190 Box::pin(async move {
191 let out = super::notify::build(&action)?;
192 let kind = if out.inform { "inform" } else { "trap" };
193 let target = out.target.clone();
194 let log_state = state.clone();
195 let send = async move {
196 let outcome = super::notify::send(&out).await;
197 if let Some(id) = server_id {
198 log_state
199 .record_access_log(
200 crate::state::AccessLogOwner::Server(id.as_u32()),
201 "SNMP",
202 None,
203 "snmp_notification_sent",
204 json!({"kind": kind, "target": out.target}),
205 vec![match &outcome {
206 Ok(result) => result.clone(),
207 Err(e) => json!({"sent": false, "error": format!("{e:#}")}),
208 }],
209 )
210 .await;
211 }
212 match outcome {
213 Ok(result) => {
214 let acknowledged = result["acknowledged"].as_bool();
215 tracing::info!(
216 "SNMP {} to {} sent{} ({})",
217 kind,
218 out.target,
219 match acknowledged {
220 Some(true) => ", acknowledged",
221 Some(false) => ", never acknowledged",
222 None => "",
223 },
224 result
225 );
226 }
227 Err(e) => tracing::warn!("SNMP {} to {} failed: {:#}", kind, out.target, e),
228 }
229 };
230 match server_id {
233 Some(id) => {
234 state.spawn_server_task(id, send).await;
235 }
236 None => send.await,
237 }
238 Ok(ActionResult::Custom {
239 name: "send_trap".into(),
240 data: json!({"kind": kind, "target": target}),
241 })
242 })
243 }
244
245 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
246 let action_type = action
247 .get("type")
248 .and_then(|v| v.as_str())
249 .context("Missing 'type' field in action")?;
250
251 match action_type {
252 "send_trap" => self.execute_send_trap(action),
253 "send_snmp_response" => self.execute_send_snmp_response(action),
254 "send_snmp_error" => self.execute_send_snmp_error(action),
255 "ignore_request" => Ok(ActionResult::NoAction),
256 "acknowledge_notification" => Ok(ActionResult::Custom {
257 name: "acknowledge_notification".into(),
258 data: json!({}),
259 }),
260 "ignore_notification" => Ok(ActionResult::NoAction),
261 _ => Err(anyhow::anyhow!("Unknown SNMP action: {}", action_type)),
262 }
263 }
264}
265
266impl SnmpProtocol {
267 fn execute_send_trap(&self, action: serde_json::Value) -> Result<ActionResult> {
270 super::notify::build(&action)?;
271 Ok(ActionResult::NoAction)
272 }
273
274 fn execute_send_snmp_response(&self, action: serde_json::Value) -> Result<ActionResult> {
276 let variables = action
277 .get("variables")
278 .and_then(|v| v.as_array())
279 .context("Missing 'variables' parameter")?;
280
281 let response_data = json!({
284 "variables": variables,
285 "error": false
286 });
287
288 Ok(ActionResult::Output(
289 serde_json::to_vec(&response_data).context("Failed to serialize SNMP response")?,
290 ))
291 }
292
293 fn execute_send_snmp_error(&self, action: serde_json::Value) -> Result<ActionResult> {
295 let error_message = action
296 .get("error_message")
297 .and_then(|v| v.as_str())
298 .context("Missing 'error_message' parameter")?;
299
300 let error_status = match action.get("error_status") {
303 None => 5,
304 Some(v) if v.is_null() => 5,
305 Some(v) => {
306 if let Some(n) = v.as_u64() {
307 if n > 5 {
308 return Err(anyhow::anyhow!(
309 "Invalid 'error_status' {n}: SNMPv1 defines 0-5 \
310 (0 noError, 1 tooBig, 2 noSuchName, 3 badValue, 4 readOnly, 5 genErr)"
311 ));
312 }
313 n as u8
314 } else if let Some(s) = v.as_str() {
315 match s.trim().to_ascii_lowercase().as_str() {
316 "noerror" => 0,
317 "toobig" => 1,
318 "nosuchname" => 2,
319 "badvalue" => 3,
320 "readonly" => 4,
321 "generr" | "genericerror" => 5,
322 other => {
323 return Err(anyhow::anyhow!(
324 "Invalid 'error_status' {other:?}: expected one of noError, \
325 tooBig, noSuchName, badValue, readOnly, genErr, or the \
326 matching code 0-5"
327 ))
328 }
329 }
330 } else {
331 return Err(anyhow::anyhow!(
332 "Invalid 'error_status': expected a name or a code 0-5, got {v}"
333 ));
334 }
335 }
336 };
337
338 let error_index = action
339 .get("error_index")
340 .and_then(|v| v.as_u64())
341 .unwrap_or(0);
342
343 let response_data = json!({
345 "error": true,
346 "error_message": error_message,
347 "error_status": error_status,
348 "error_index": error_index
349 });
350
351 Ok(ActionResult::Output(
352 serde_json::to_vec(&response_data).context("Failed to serialize SNMP error")?,
353 ))
354 }
355}
356
357fn param(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
358 Parameter {
359 name: name.to_string(),
360 type_hint: type_hint.to_string(),
361 description: description.to_string(),
362 required,
363 }
364}
365
366fn send_trap_action() -> ActionDefinition {
368 ActionDefinition {
369 name: "send_trap".to_string(),
370 description: "Send an SNMP notification to a manager: a v2c trap (default), a v2c inform that is resent until the manager acknowledges it, or a v1 trap. sysUpTime.0 and snmpTrapOID.0 are added for v2c; the result is logged".to_string(),
371 parameters: vec![
372 param("target", "string", "The manager: host or host:port (port 162 when omitted)", true),
373 param("trap_oid", "string", "v2c: the notification's OID (snmpTrapOID.0), e.g. 1.3.6.1.6.3.1.1.5.3 linkDown, 1.3.6.1.6.3.1.1.5.4 linkUp", false),
374 param("variables", "array", "Variable bindings [{oid, type, value}]; type is integer, string (encoding hex for binary), oid, ipaddress, counter, gauge, timeticks, counter64 (v2c only) or null", false),
375 param("version", "string", "v2c (default) or v1", false),
376 param("inform", "boolean", "v2c: send an InformRequest and wait for the manager's acknowledgement (default false: a trap)", false),
377 param("community", "string", "Community string (default public)", false),
378 param("uptime", "number", "sysUpTime / time-stamp in hundredths of a second (default: time since NetGet started)", false),
379 param("enterprise", "string", "v1: the enterprise OID (default 1.3.6.1.4.1.8072.9999)", false),
380 param("generic_trap", "number", "v1: 0 coldStart, 1 warmStart, 2 linkDown, 3 linkUp, 4 authenticationFailure, 5 egpNeighborLoss, 6 enterpriseSpecific (default)", false),
381 param("specific_trap", "number", "v1: the enterprise-specific trap number (default 0)", false),
382 param("agent_addr", "string", "v1: the agent's IPv4 address in the trap (default 0.0.0.0)", false),
383 ],
384 example: json!({
385 "type": "send_trap",
386 "target": "127.0.0.1:162",
387 "trap_oid": "1.3.6.1.6.3.1.1.5.3",
388 "variables": [
389 {"oid": "1.3.6.1.2.1.2.2.1.1.2", "type": "integer", "value": 2}
390 ]
391 }),
392 log_template: Some(
393 LogTemplate::new()
394 .with_info("-> SNMP notification to {target}")
395 .with_debug("SNMP send_trap: target={target}, {variables_len} vars"),
396 ),
397 }
398}
399
400fn acknowledge_notification_action() -> ActionDefinition {
401 ActionDefinition {
402 name: "acknowledge_notification".to_string(),
403 description: "Accept the notification. For an inform this sends the Response the sender is waiting for; for a trap nothing goes on the wire".to_string(),
404 parameters: vec![],
405 example: json!({"type": "acknowledge_notification"}),
406 log_template: Some(
407 LogTemplate::new()
408 .with_info("SNMP notification acknowledged")
409 .with_debug("SNMP acknowledge_notification"),
410 ),
411 }
412}
413
414fn ignore_notification_action() -> ActionDefinition {
415 ActionDefinition {
416 name: "ignore_notification".to_string(),
417 description:
418 "Do not accept it: an inform gets no Response, so its sender retries and then gives up"
419 .to_string(),
420 parameters: vec![],
421 example: json!({"type": "ignore_notification"}),
422 log_template: Some(
423 LogTemplate::new()
424 .with_info("SNMP notification ignored")
425 .with_debug("SNMP ignore_notification"),
426 ),
427 }
428}
429
430pub static SNMP_NOTIFICATION_EVENT: LazyLock<EventType> =
431 LazyLock::new(|| {
432 EventType::new(
433 "snmp_notification",
434 "A trap or inform arrived from an agent",
435 json!({"type": "acknowledge_notification"}),
436 )
437 .with_parameters(vec![
438 param(
439 "kind",
440 "string",
441 "trap or inform (an inform waits for acknowledgement)",
442 true,
443 ),
444 param("version", "string", "The SNMP version the notification used: v1 or v2c", true),
445 param(
446 "community",
447 "string",
448 "Community string the sender used",
449 true,
450 ),
451 param(
452 "trap_oid",
453 "string",
454 "v2c: snmpTrapOID.0, the notification's identity (e.g. 1.3.6.1.6.3.1.1.5.3 linkDown)",
455 false,
456 ),
457 param(
458 "uptime",
459 "number",
460 "The sender's sysUpTime (v2c) or time-stamp (v1), hundredths of a second",
461 false,
462 ),
463 param("enterprise", "string", "v1: the enterprise OID", false),
464 param(
465 "agent_addr",
466 "string",
467 "v1: the agent address the trap names",
468 false,
469 ),
470 param(
471 "generic_trap",
472 "number",
473 "v1: 0 coldStart … 6 enterpriseSpecific",
474 false,
475 ),
476 param(
477 "specific_trap",
478 "number",
479 "v1: the enterprise-specific trap number",
480 false,
481 ),
482 param(
483 "variables",
484 "array",
485 "The other variable bindings [{oid, type, value}] (binary strings carry encoding hex)",
486 true,
487 ),
488 param(
489 "request_id",
490 "number",
491 "v2c: the PDU's request id (an inform's acknowledgement echoes it automatically)",
492 false,
493 ),
494 param(
495 "client_ip",
496 "string",
497 "Where the notification came from",
498 true,
499 ),
500 ])
501 .with_actions(vec![
502 acknowledge_notification_action(),
503 ignore_notification_action(),
504 send_trap_action(),
505 ])
506 .with_log_template(
507 LogTemplate::new()
508 .with_info("SNMP {kind} from {client_ip}")
509 .with_debug("SNMP {version} {kind} from {client_ip}: {trap_oid} {variables}")
510 .with_trace("SNMP: {json_pretty(.)}"),
511 )
512 });
513
514fn send_snmp_response_action() -> ActionDefinition {
516 ActionDefinition {
517 name: "send_snmp_response".to_string(),
518 description: "Answer the request with a set of OID values. The request-id, community string and SNMP version are copied from the request being answered, so you only supply the values. Return exactly one entry per OID in the event's 'oids' list, in the same order, and no others; each value is the one your instructions give for that OID, word for word. The example's OID and value are placeholders for the shape, never data.".to_string(),
519 parameters: vec![Parameter {
520 name: "variables".to_string(),
521 type_hint: "array".to_string(),
522 description: "Variable bindings, each an object {\"oid\": ..., \"type\": ..., \"value\": ...}. 'oid' is dotted decimal such as \"1.3.6.1.2.1.1.1.0\" (first component 0-2, every component numeric). 'type' is one of: \"string\" (text), \"integer\" (signed 32-bit), \"counter\" (Counter32, monotonically increasing), \"gauge\" (Gauge32, a value that goes up and down), \"timeticks\" (hundredths of a second since start-up, e.g. sysUpTime), \"null\" (no value for this OID). 'value' must match the type: a JSON string for \"string\", a JSON number for the numeric types, omitted for \"null\". An unrecognised type encodes as NULL".to_string(),
523 required: true,
524 }],
525 example: json!({
526 "type": "send_snmp_response",
527 "variables": [
528 {"oid": "1.3.6.1.4.1.99999.1.0", "type": "string", "value": "<the value your instructions give for this OID>"}
529 ]
530 }),
531 log_template: Some(
532 LogTemplate::new()
533 .with_info("-> SNMP response {variables_len} vars")
534 .with_debug("SNMP send_snmp_response: {variables_len} variables"),
535 ),
536 }
537}
538
539fn send_snmp_error_action() -> ActionDefinition {
541 ActionDefinition {
542 name: "send_snmp_error".to_string(),
543 description: "Answer the request with an SNMP error instead of values - use it when the requested OID does not exist here, or when a SET is refused. The reply carries the numeric error-status and no variable bindings; the request-id and community are echoed automatically.".to_string(),
544 parameters: vec![
545 Parameter {
546 name: "error_message".to_string(),
547 type_hint: "string".to_string(),
548 description: "Why the request failed. SNMP has no field for text, so this only reaches NetGet's logs and access log - the client sees 'error_status'".to_string(),
549 required: true,
550 },
551 Parameter {
552 name: "error_status".to_string(),
553 type_hint: "string | number".to_string(),
554 description: "Which error the client is told about: \"noSuchName\" (2) for an OID this agent does not serve, \"badValue\" (3) for a SET with an unusable value, \"readOnly\" (4) for a SET of a read-only OID, \"tooBig\" (1) if the answer would not fit, \"genErr\" (5) for anything else. Accepts the name or the number. Default: genErr".to_string(),
555 required: false,
556 },
557 Parameter {
558 name: "error_index".to_string(),
559 type_hint: "number".to_string(),
560 description: "1-based position of the offending OID in the request's list, or 0 when no single OID is to blame. Default 0".to_string(),
561 required: false,
562 },
563 ],
564 example: json!({
565 "type": "send_snmp_error",
566 "error_message": "OID 1.3.6.1.4.1.9999.1.0 is not served by this agent",
567 "error_status": "noSuchName",
568 "error_index": 1
569 }),
570 log_template: Some(
571 LogTemplate::new()
572 .with_info("-> SNMP error: {error_message}")
573 .with_debug("SNMP send_snmp_error: {error_message}"),
574 ),
575 }
576}
577
578fn ignore_request_action() -> ActionDefinition {
580 ActionDefinition {
581 name: "ignore_request".to_string(),
582 description: "Ignore this SNMP request and don't send a response".to_string(),
583 parameters: vec![],
584 example: json!({
585 "type": "ignore_request"
586 }),
587 log_template: Some(
588 LogTemplate::new()
589 .with_info("SNMP request ignored")
590 .with_debug("SNMP ignore_request"),
591 ),
592 }
593}
594
595pub static SNMP_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
600 EventType::new(
601 "snmp_request",
602 "SNMP client sent a GET/GETNEXT/GETBULK request",
603 json!({
604 "type": "send_snmp_response",
605 "variables": [
606 {"oid": "1.3.6.1.4.1.99999.1.0", "type": "string", "value": "<the value your instructions give for this OID>"}
607 ]
608 })
609 )
610 .with_parameters(vec![
611 Parameter {
612 name: "request_type".to_string(),
613 type_hint: "string".to_string(),
614 description: "PDU type of the request, spelled exactly: 'GetRequest', 'GetNextRequest', 'GetBulkRequest' or 'SetRequest'. GetNextRequest asks for the OID that follows the one given (table walking); GetBulkRequest asks for several at once".to_string(),
615 required: true,
616 },
617 Parameter {
618 name: "oids".to_string(),
619 type_hint: "array".to_string(),
620 description: "OIDs the client asked about, in dotted decimal, in request order. Answer with one variable binding per OID in the same order".to_string(),
621 required: true,
622 },
623 Parameter {
624 name: "answer_with".to_string(),
625 type_hint: "string".to_string(),
626 description: "Which answer this request takes: the variables to return, each OID named with its MIB-2 name where it has one (sysDescr, sysName, ...), and when to answer noSuchName instead".to_string(),
627 required: true,
628 },
629 Parameter {
630 name: "community".to_string(),
631 type_hint: "string".to_string(),
632 description: "Community string the client sent - SNMPv1/v2c's only credential, typically 'public'. It is echoed back automatically; check it here if the instruction says to reject unknown communities".to_string(),
633 required: false,
634 },
635 Parameter {
636 name: "request_id".to_string(),
637 type_hint: "number".to_string(),
638 description: "Request id chosen by the client. The response echoes it automatically - it is exposed only for logging".to_string(),
639 required: false,
640 },
641 Parameter {
642 name: "version".to_string(),
643 type_hint: "string".to_string(),
644 description: "'v1', 'v2c' or 'v3'. The reply uses the same version".to_string(),
645 required: false,
646 },
647 Parameter {
648 name: "client_ip".to_string(),
649 type_hint: "string".to_string(),
650 description: "IP address the request came from".to_string(),
651 required: false,
652 },
653 Parameter {
654 name: "user".to_string(),
655 type_hint: "string".to_string(),
656 description: "SNMPv3 only: the USM user the request was authenticated as (its MAC verified and its time checked before you see it). Decide per user what it may read or write".to_string(),
657 required: false,
658 },
659 Parameter {
660 name: "security_level".to_string(),
661 type_hint: "string".to_string(),
662 description: "SNMPv3 only: noAuthNoPriv, authNoPriv or authPriv - always the level the user is configured for. The answer goes back at the same level, encrypted when authPriv".to_string(),
663 required: false,
664 },
665 Parameter {
666 name: "context_name".to_string(),
667 type_hint: "string".to_string(),
668 description: "SNMPv3 only: the contextName the manager asked in, empty for the default context".to_string(),
669 required: false,
670 },
671 ])
672 .with_actions(vec![
673 send_snmp_response_action(),
674 send_snmp_error_action(),
675 ignore_request_action(),
676 send_trap_action(),
677 ])
678 .with_log_template(
679 LogTemplate::new()
680 .with_info("SNMP {request_type} from {client_ip}")
681 .with_debug("SNMP {request_type} from {client_ip} ({version}, community={community}): {oids}")
682 .with_trace("SNMP: {json_pretty(.)}"),
683 )
684});
685
686fn well_known_oid(oid: &str) -> Option<&'static str> {
691 Some(match oid.trim_start_matches('.') {
692 "1.3.6.1.2.1.1.1.0" => "sysDescr, the system description your instructions give",
693 "1.3.6.1.2.1.1.2.0" => "sysObjectID, the vendor's object identifier",
694 "1.3.6.1.2.1.1.3.0" => "sysUpTime, timeticks since start-up",
695 "1.3.6.1.2.1.1.4.0" => "sysContact, the contact person",
696 "1.3.6.1.2.1.1.5.0" => {
697 "sysName, the device's name exactly as your instructions write it - a hostname, \
698 character for character, never reworded or capitalised"
699 }
700 "1.3.6.1.2.1.1.6.0" => "sysLocation, where the system is",
701 "1.3.6.1.2.1.1.7.0" => "sysServices, an integer",
702 "1.3.6.1.2.1.2.1.0" => "ifNumber, how many interfaces",
703 _ => return None,
704 })
705}
706
707pub fn answer_with_for_request(request_type: &str, oids: &[String]) -> String {
715 let named: Vec<String> = oids
716 .iter()
717 .map(|oid| match well_known_oid(oid) {
718 Some(name) => format!("{oid} ({name})"),
719 None => oid.clone(),
720 })
721 .collect();
722 let list = named.join(", ");
723 let n = oids.len();
724 let noun = if n == 1 { "variable" } else { "variables" };
725 match request_type {
726 "GetNextRequest" | "GetBulkRequest" => format!(
727 "send_snmp_response with the OID that follows each of {list} in your agent's tree, \
728 and its value; send_snmp_error with noSuchName when nothing follows"
729 ),
730 "SetRequest" => format!(
731 "send_snmp_response echoing the {n} {noun} set on {list} when your instructions let \
732 them be written; send_snmp_error with readOnly when they do not"
733 ),
734 _ => format!(
735 "send_snmp_response with exactly {n} {noun}, in this order: {list}. Each value is \
736 the one your instructions give for it, word for word; add no other OIDs. If your \
737 instructions give no value for one of them, send_snmp_error with noSuchName and \
738 its 1-based error_index instead"
739 ),
740 }
741}
742
743pub fn get_snmp_event_types() -> Vec<EventType> {
744 vec![SNMP_REQUEST_EVENT.clone(), SNMP_NOTIFICATION_EVENT.clone()]
745}