trishul-snmp

trishul-snmp — Architecture

Last updated: 2026-06-04


1. Overview

trishul-snmp is a package-first SNMP runtime. The core runtime handles wire codec, UDP transport, request dispatch, manager operations, outbound notification send, inbound notification receive, and a narrow read-only responder with no MIB compiler dependency. Optional compiled-JSON artifacts add symbolic translation and richer display.

┌──────────────────────────────────────────────────────────────────────────────┐
│                             Python API / CLI                                 │
│  V2cManager / V3Manager  ·  V2cNotifier / V3Notifier                        │
│  V2cNotificationListener / V3NotificationListener  ·  V2cResponder          │
│  decode_notification()                                                       │
├──────────────────────────────────────────────────────────────────────────────┤
│ manager/      target normalization, request shaping, walk logic              │
│ notify/       send, listen, and offline notification decode                  │
│ responder/    read-only request handling and simulator sources               │
│ security/     SecurityModel protocol · CommunityModel · UsmModel (v3 USM)   │
│ session.py    shared UdpClient + Dispatcher + Lock + MibBundle               │
│ transport/    UDP client/server, request matching, retries                   │
│ wire/         BER / ASN.1 / SNMPv2c + SNMPv3 message + PDU codec            │
│ mib/          optional bundle loading, registry, rendering                   │
└──────────────────────────────────────────────────────────────────────────────┘

The CLI is intentionally thin. It does not define a second architecture.


2. Package structure

trishul_snmp/
├── __init__.py          ← public package surface + version
├── __main__.py          ← `python -m trishul_snmp`
├── errors.py            ← exception hierarchy (incl. AuthenticationError)
├── types.py             ← public response and SNMP value models
├── session.py           ← shared UdpClient + Dispatcher + Lock + MibBundle
│
├── security/
│   ├── model.py         ← SecurityModel protocol (structural)
│   ├── community.py     ← CommunityModel for SNMPv2c
│   └── usm.py           ← UsmModel, UsmUser, UsmLocalEngine, auth/priv protocols
│
├── wire/
│   ├── ber.py           ← BER primitives
│   ├── asn1.py          ← ASN.1 value encoding helpers
│   ├── message.py       ← SNMPv2c message encode/decode
│   ├── v3message.py     ← SNMPv3 outer message + ScopedPDU + USM params codec
│   └── pdu.py           ← PDU models and PDU encode/decode
│
├── transport/
│   ├── udp.py           ← connected UDP client
│   └── dispatcher.py    ← request ids, timeout/retry, response matching
│
├── manager/
│   ├── client.py        ← SnmpManager base · V2cManager · V3Manager
│   ├── operations.py    ← target normalization and response shaping
│   └── walk.py          ← subtree walk stop rules and iteration
│
├── notify/
│   ├── client.py        ← SnmpNotifier base · V2cNotifier · V3Notifier
│   ├── listener.py      ← V2c/V3 notification listener public receive APIs
│   ├── v3.py            ← listener-side v3 decode/report/response helpers
│   ├── events.py        ← notification event model + live/offline decode
│   └── __init__.py      ← notification package export
│
├── responder/
│   ├── server.py        ← V2cResponder public API
│   ├── sources.py       ← in-memory and callback-backed data sources
│   ├── rules.py         ← simulation rules for dynamic OID values
│   └── __init__.py      ← responder package export
│
├── mib/
│   ├── loader.py        ← bundle file/directory loading
│   ├── bundle.py        ← public MibBundle abstraction
│   ├── registry.py      ← symbol and OID lookup registry
│   ├── models.py        ← normalized compiled-JSON records
│   └── render.py        ← varbind enrichment and display rendering
│
└── cli/
    ├── main.py          ← argument parser and command handlers
    ├── common.py        ← shared options, bundle loading, value parsing
    └── output.py        ← manager and notification text/JSON rendering

3. Layer responsibilities

3.1 wire/

Pure protocol codec. Responsibilities:

Non-responsibilities:

3.1a security/

Security model abstraction. Responsibilities:

UsmModel imports cryptography lazily inside auth/priv methods only; the class is always importable without the [v3] extra.

3.2 transport/

Owns request/response transport behavior:

3.3 manager/

Owns the public runtime behavior:

3.4 notify/

Owns notification-specific runtime behavior:

3.5 responder/

Owns the narrow read-only simulator behavior:

3.6 mib/

Owns optional symbolic services:

3.7 cli/

Owns command-line UX only:


4. End-to-end call flows

4.1 Numeric GET with no bundle

  1. Caller invokes await manager.get("1.3.6.1.2.1.1.3.0").
  2. normalize_targets() parses the numeric OID.
  3. build_request_varbinds() creates NULL placeholder varbinds.
  4. RequestDispatcher.send_pdu() assigns a request id, encodes the SNMP message, and sends it over UDP.
  5. UdpClient.receive() waits for a matching response with timeout/retry handling.
  6. decode_message() decodes the response and response_from_pdu() builds the public Response.
  7. With no bundle loaded, enrichment is effectively pass-through.

This flow is shown with V2cManager. V3Manager follows the same request path after SnmpSession.open() performs engine discovery and UsmModel wraps/unwraps the PDU.

4.2 Symbolic GET with a bundle

  1. Caller loads a bundle once via load_bundle(path).
  2. normalize_targets() resolves MODULE::symbol input through MibBundle.resolve().
  3. Network I/O remains numeric only.
  4. After response decode, enrich_varbinds() uses bundle lookup and render helpers to populate display_name and display_value.

4.3 Walk / bulkwalk

  1. walk() resolves the root once at the API boundary.
  2. walk_subtree() iterates via GETNEXT or GETBULK.
  3. Walk stops when the response leaves the subtree, repeats or decreases OIDs, or returns endOfMibView.
  4. The final result is a tuple of VarBind objects, optionally enriched by the bundle.

4.4 Offline translation

  1. load_bundle() builds a MibRegistry from compiled JSON artifacts.
  2. bundle.translate(), bundle.resolve(), and bundle.lookup() operate with no network I/O.
  3. A single module JSON file is sufficient for narrow translation use cases.

4.5 Outbound trap/inform send

  1. Caller invokes await V2cNotifier.send_trap(...), await V2cNotifier.send_inform(...), await V3Notifier.send_trap(...), or await V3Notifier.send_inform(...).
  2. Numeric or symbolic notification OIDs are normalized at the API edge.
  3. sysUpTime.0 and snmpTrapOID.0 are inserted first unless explicitly provided.
  4. The notification PDU is encoded and sent over UDP.
  5. Trap send stops after send; inform send waits for a matching RESPONSE PDU.
  6. V3Notifier.send_inform() uses peer-discovered receiver engine state. V3Notifier.send_trap() requires explicit UsmLocalEngine so the outbound message carries the sender’s own authoritative engine state and does not depend on peer discovery during open().

4.6 Inbound trap/inform receive

  1. Caller opens V2cNotificationListener(...) or V3NotificationListener(...).
  2. UdpServer binds the requested host and port.
  3. The listener receives inbound datagrams and decodes SNMP messages.
  4. For v2c, non-notification PDUs and filtered communities are ignored.
  5. For v3, discovery probes are answered with REPORTs and malformed/wrong-user/auth-failed datagrams are dropped quietly.
  6. Informs are acknowledged automatically with a matching RESPONSE PDU.
  7. The listener returns a NotificationEvent carrying source address, PDU kind, decoded varbinds, notification metadata, and additive v3 security metadata when present.

4.7 Offline trap/inform decode

  1. Caller invokes decode_notification(raw_bytes, bundle=...) for v2c or decode_notification(raw_bytes, bundle=..., user=...) for strict v3 USM decode.
  2. The function routes through the v2c or v3 codec path based on whether user was supplied.
  3. The low-level decode builds the public NotificationEvent.
  4. If a bundle is loaded, snmpTrapOID.0 is reverse-looked-up into notification_name and declared member_bindings.
  5. No UDP transport or dispatcher code is involved.

4.8 Read-only responder flow

  1. Caller configures V2cResponder with an in-memory or callback-backed source.
  2. UdpServer binds the requested host and port.
  3. The responder receives inbound SNMPv2c messages and filters by community.
  4. GET, GET_NEXT, and GET_BULK are answered from the configured source using lexicographic OID ordering.
  5. Missing exact objects become noSuchObject; next/bulk exhaustion becomes endOfMibView.
  6. The responder sends a matching RESPONSE PDU back to the request source address.

5. Bundle boundary

The runtime/compiler split is a deliberate architectural boundary:

This keeps deployment simple and lets callers supply only the compiled JSON they actually need.


6. Scope guardrails

The current main-branch scope is still intentionally narrower than a full SNMP stack:

Raw MIB ingestion, compiler workflows, writable set, SNMPv1, and full agent framework support remain outside the current implemented architecture.