Imported from dvaerum/snmp-trap-event-handler (
AGENTS.md). Install upstream withnpx skills add dvaerum/snmp-trap-event-handler. Copyright stays with the author.
AGENTS.md
Guidance for AI coding agents working in this repository.
Project Overview
Rust SNMP trap event handler — receives SNMP traps, parses them, executes configurable hooks, and optionally broadcasts events via SSE. Two independent crates in crates/, no workspace root Cargo.toml.
Stack: Rust, Tokio, serde, anyhow, tracing, snmp-parser. Nix flake for builds, NixOS module for deployment.
Build / Lint / Test Commands
All cargo commands must run from within the crate directory.
# Primary crate
cd crates/snmp-trap-event-handler
cargo build
cargo test # all tests
cargo test test_format_mac # single test by name substring
cargo test config::tests # all tests in a module
cargo clippy -- -D warnings # lint (treat warnings as errors)
cargo fmt # format
cargo fmt --check # check formatting (CI)
# Run with config (use --release to avoid stack overflow on SNMP queries)
cargo run --release -- -c ../../examples/config.example.json
RUST_LOG=debug cargo run --release -- -c ../../examples/config.example.json
# Secondary crate
cd crates/updated-kea-cli
cargo test
# Nix (from repo root)
nix build
nix flake check # runs all checks
Code Style
Formatting
Default rustfmt (no .rustfmt.toml). 4-space indentation. Run cargo fmt before committing.
Import Ordering
Group imports in this order, each group alphabetized internally:
- External crates (
anyhow,serde,tokio,tracing, …) - Standard library (
std::collections,std::net, …) - Crate-local (
crate::config,crate::hook_executor, …)
use anyhow::{Context, Result};
use snmp_parser::{parse_snmp_v2c, SnmpPdu};
use std::collections::HashMap;
use std::net::SocketAddr;
use tokio::sync::broadcast;
use tracing::{debug, error, info, warn};
use crate::config::Config;
use crate::hook_executor::TrapEvent;
Naming
| Kind | Convention | Example |
|---|---|---|
| Modules | snake_case |
trap_handler, snmp_client |
| Structs/Enums | PascalCase |
TrapHandler, MacNotifyAction |
| Enum variants | PascalCase |
LinkUp, Unknown(String) |
| Functions/vars | snake_case |
handle_trap, hook_manager |
| Constants | SCREAMING_SNAKE_CASE |
LINKUP_OID, FDB_PORT_OID |
Module Organization
- Each module is a directory with
mod.rs(notmodule_name.rsstyle). - Items are private by default; add
pubonly when needed across modules. #[allow(dead_code)]is acceptable for fields kept for planned use.
Error Handling
- Use
anyhow::Result<T>for fallible functions. - Add context with
.with_context(|| format!("…"))or.context("…"). - Use
anyhow::bail!("…")for early error returns. - Never
unwrap()in production code. Useexpect("reason")only for true invariants. - In tests,
unwrap()is fine.
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config: {}", path.display()))?;
Async Patterns
- Runtime:
#[tokio::main]onmain,#[tokio::test]on async tests. - Shared state:
Arc<RwLock<T>>(tokio's async RwLock). - Concurrency:
tokio::spawnfor independent tasks,tokio::select!for racing. - Timeouts:
tokio::time::timeoutwrapping async operations.
Logging
Use tracing macros: debug!, info!, warn!, error!.
use tracing::{debug, info, warn, error};
debug!("Parsed SNMPv2c: version={}", snmp_msg.version);
info!("Starting trap handler on {}", addr);
warn!("Unknown trap OID {} from {}", oid, source);
error!("Hook execution failed for '{}': {}", name, e);
Serde / Config Patterns
- Derive
Deserialize, Serializeon config types; addDebug, Clone. - Use
#[serde(default = "fn_name")]for defaults, notDefaultimpl. - Use
#[serde(skip_serializing_if = "Option::is_none")]for optional fields. - Validate config in a
validate()method called fromload().
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SnmpClient {
pub community: String,
#[serde(default = "default_timeout")]
pub timeout_ms: u64,
}
fn default_timeout() -> u64 { 5000 }
Types
Arc<T>for shared ownership across async tasks.HashMap<K, V>for key-value maps;VecDeque<T>for ring buffers.tokio::sync::broadcastfor event fan-out (SSE).- Derive
Cloneon types passed between async tasks.
Tests
Tests live in #[cfg(test)] mod tests at the bottom of each source file.
#[cfg(test)]
mod tests {
use super::*;
fn create_test_event(trap_type: &str) -> TrapEvent { /* … */ }
#[test]
fn test_format_mac_address_6_bytes() {
let result = format_mac_address(&val);
assert_eq!(result, Some("aa:bb:cc:dd:ee:ff".to_string()));
}
#[tokio::test]
async fn test_store_and_get_sources() {
let store = TrapStore::new(100, vec![]);
store.store(create_test_event("linkup", "10.0.0.1:162")).await;
assert_eq!(store.get_sources().await.len(), 1);
}
}
Conventions:
- Name tests
test_<what>_<scenario>. - Use helper functions (
create_test_*) for reusable test data. assert_eq!for values,assert!(matches!(…))for enum variants,assert!(result.is_err())for failures.
Comments
///doc comments on public structs, enums, and functions.//!module-level doc comments at top ofmod.rs.- Inline
//comments for non-obvious logic only.
Workflow
Always commit after making changes. Create a commit with a descriptive message summarizing the changes whenever you add or modify code, docs, or tests.
Architecture Quick Reference
Data flow: UDP trap → parse SNMPv1/v2c → match handler → immediate hooks → SNMP queries → broadcast SSE → store → waiting hooks
Key modules in crates/snmp-trap-event-handler/src/:
| Module | Purpose |
|---|---|
config/ |
JSON config loading, validation |
trap_listener/ |
UDP socket, interface→IP resolution |
trap_handler/ |
SNMP parsing, OID matching, data extraction |
trap_store/ |
In-memory per-source ring buffer history |
known_traps/ |
Registry of known enterprise traps |
snmp_client/ |
Async SNMP GET/WALK via snmp2 |
snmp_queries/ |
Query execution & predefined query sets |
hook_executor/ |
Spawn external commands with timeout, env vars |
hook_manager/ |
Inline + extra hooks, hot-reload via watch |
extra_hooks/ |
Load hooks from dirs, file watching |
api/ |
REST API with SSE event stream |
Nix Commands
# Development shell
nix develop
# Build packages
nix build # Main snmp-trap-event-handler
nix build .#updated-kea-cli # Build Kea CLI tool
# Run checks (including all tests)
nix flake check
Additional Development Commands
# Test SNMP connectivity
cargo run --release --example query_device -- <device_ip> <community>
# Test REST API endpoints (with API enabled in config)
curl http://localhost:4002/health # Health check
curl http://localhost:4002/sources # List all trap sources
curl http://localhost:4002/history/<source_ip> # Get trap history
curl http://localhost:4002/info/<source_ip> # Get current state
curl -N http://localhost:4002/events # SSE event stream
# Test trap sending
./scripts/send-test-trap.py <target_ip> <port>
Common Development Workflows
Adding a New Known Trap Type
- Create a new module in
known_traps/(e.g.,vendor_trap.rs) - Define constants:
NAME,ENTERPRISE_OID, and mappings struct - Add the definition to
KNOWN_TRAPSarray inknown_traps/mod.rs - Add name to
KNOWN_TRAP_NAMESarray - Update
examples/config.example.json - Add tests
Adding a Hook
- Create script in
examples/hooks/ - Make executable (
chmod +x) - Add to config under appropriate handler's
hooksarray - Test with manual env vars:
TRAP_MAC_ADDRESS=aa:bb:cc:dd:ee:ff TRAP_ACTION=learned ./script.sh
Adding a New Template Variable
- Define in
TrapDatastruct intrap_handler/mod.rs - Add substitution in
hook_executor/mod.rs(substitute_template_vars) - Add to env vars in
hook_executor/mod.rs(build_env_vars)
Adding a Predefined Query Set
- Define static array of
PredefinedQueryinsnmp_queries/predefined.rs - Add entry to
PREDEFINED_SETSregistry - Add tests for the new set