SNMP-Trap-Tools(at)Work

SNMP-Trap-Tools(at)Work / Technical description

SNMP Trap Receiver

Receiver for SNMP traps and informs in v1, v2c and v3 with a web GUI for Rocky Linux 9

Version 2.1.0Revised 2026-10-02

1. Summary

SNMP Trap Receiver receives SNMP notifications (traps and informs in SNMP v1, v2c and v3) from network equipment and servers. It translates the numeric object IDs into names using MIB files, stores every notification in log files and shows them continuously in a web GUI in English or Norwegian.

Version 2.0 replaces the earlier Docker container. It runs as a hardened systemd service on Rocky Linux 9 (including the minimal installation) and is installed from a single self-contained file without internet access.

2. Purpose and scope

Receives

SNMPv1 Trap-PDU, SNMPv2c Trap and InformRequest, SNMPv3 Trap and InformRequest (USM: noAuthNoPriv, authNoPriv, authPriv) over UDP/IPv4 and UDP/IPv6

Translates

Trap OID and variable names using uploaded vendor MIBs and the built-in IETF MIBs

Stores

Human-readable text log and JSON Lines log, both rotated

Shows

Live list with filters, details per trap, CSV export, receiver status, MIB management, log download and license information

Does not

Forward traps, send e-mail, poll devices or store traps in a database

3. Architecture

  network devices                     Rocky Linux 9 server
 ┌───────────────┐  UDP 162   ┌────────────────────────────────────────────────┐
 │ switch/router ├──────────► │  snmp-trap-receiver.service  (user snmptrap)   │
 │ ESXi, UPS ... │  v1/v2c/v3 │                                                │
 └───────────────┘            │  ┌────────────────────┐   ┌─────────────────┐  │
                              │  │ TrapReceiver       │   │ MibManager      │  │
                              │  │ thread + asyncio   ├──►│ MIB compiler    │  │
                              │  │ pysnmp 7 engine    │   │ OID→name lookup │  │
                              │  └─────────┬──────────┘   └─────────────────┘  │
                              │            │ record                            │
                              │  ring buffer (2000)  +  snmp_traps.log/.jsonl  │
                              │            ▲                                   │
                              │  ┌─────────┴──────────┐                        │
 ┌──────────┐ HTTP 8080       │  │ Flask app on       │                        │
 │ browser  ├───────────────► │  │ Waitress, 8 threads│                        │
 └──────────┘ GUI + JSON API  │  └────────────────────┘                        │
                              └────────────────────────────────────────────────┘

Everything runs in a single process. The SNMP engine has its own thread with its own asyncio event loop. The web server runs in the Waitress worker threads and reads from the receiver’s in-memory ring buffer. The two meet only in a lock-protected queue, so a slow browser never delays the reception of traps.

3.1. Modules

Module Responsibility

trapreceiver/config.py

Reads /etc/snmp-trap-receiver/config.ini. All keys have safe defaults. Also updates individual keys while preserving comments and file ownership.

trapreceiver/receiver.py

TrapReceiver: builds the pysnmp engine, binds transports and checks the bindings, processes notifications, writes logs and maintains the ring buffer and statistics. Can be started, stopped and restarted while the process keeps running.

trapreceiver/mibs.py

MibManager: validates and stores uploads, compiles MIBs offline and looks up names for OIDs.

trapreceiver/web.py

The Flask application: login, CSRF, security headers, pages and JSON API.

trapreceiver/i18n.py

All GUI texts in English and Norwegian (121 keys). Both languages must have the same keys, otherwise the module will not load.

trapreceiver/manual.py

Shows the user manual (docs/no/Brukermanual.md, docs/en/User-Manual.md) in the GUI. A small Markdown parser with no dependencies: all text is HTML-escaped first, and links can only point to #ankre or http(s).

trapreceiver/selftest.py

End-to-end test on a temporary port and in temporary directories.

trapreceiver/main.py

Command line: serve, set-password, compile-mibs, import-mibs, selftest, version.

trapreceiver/mibs_base/

25 standard IETF MIB sources plus a stub for RFC-1212.

trapreceiver/templates/, static/

HTML templates, Bootstrap 5.3.7 and the application’s own JavaScript and CSS.

4. Trap processing

4.1. Reception

For each listen entry the receiver opens a UDP transport (IPv4 or IPv6) in pysnmp. pysnmp binds sockets asynchronously. The receiver therefore waits for each binding and reports every error, for example Address already in use or Address family not supported. If no address can be bound, the receiver remains stopped and shows the error. If only some can be bound, it runs on those and shows a warning.

Communities are registered as security entries for SNMPv1/v2c. SNMPv3 users are registered in the USM with their authentication and privacy protocols. A v3 trap is accepted only if the user is registered for the sender’s snmpEngineID (engine_id in the configuration). A v3 inform uses the receiver’s own engine ID and does not need engine_id.

4.2. Community check

An observer on the pysnmp execution point rfc2576.processIncomingMsg:writable sees every v1/v2c message before it is authorised:

  • The community actually used is remembered for the log.

  • If it is unknown and accept_any_community = false, pysnmp discards the message. The receiver counts it as rejected and writes a REJECTED line with the sender address to the text log.

  • If accept_any_community = true, the community is mapped to the first configured one so that the message is accepted. The log still shows the community the device actually sent.

4.3. Notification processing

NotificationReceiver in pysnmp calls the receiver for every accepted trap or inform and acknowledges informs automatically. The receiver then does the following:

  1. gets the source address and port from the transport information;

  2. gets the SNMP version and PDU type (trap or inform) from the message context;

  3. looks up each variable binding in the MIB view (name and value formatting, for example enumerations and DisplayString);

  4. extracts snmpTrapOID.0 and sysUpTime.0. For SNMPv1 traps pysnmp has already converted the Trap-PDU into the SNMPv2 form, so snmpTrapAddress.0, snmpTrapCommunity.0 and snmpTrapEnterprise.0 are included as variables;

  5. builds a record, adds it to the ring buffer and writes it to both log files.

4.4. Record format

Each line in snmp_traps.jsonl is one JSON object:

{"id": 17, "time": "2026-10-02 10:45:20", "ts": 1790930720.642,
 "source": "192.0.2.31", "source_port": 49052, "version": "v2c",
 "kind": "trap", "security": "public",
 "trap_oid": "1.3.6.1.6.3.1.1.5.3", "trap_name": "IF-MIB::linkDown",
 "uptime": "123456",
 "varbinds": [
   {"oid": "1.3.6.1.2.1.2.2.1.1.12", "name": "IF-MIB::ifIndex.12",
    "value": "12", "type": "Integer"},
   {"oid": "1.3.6.1.2.1.2.2.1.8.12", "name": "IF-MIB::ifOperStatus.12",
    "value": "down", "type": "Integer"}]}

id

Sequence number. Continues after a restart, because the buffer is reloaded from the JSON log

security

Community (v1/v2c) or USM user name (v3)

kind

trap or inform

name

Empty if no loaded MIB defines the OID

The text log has the same content in human-readable form:

[2026-10-02 10:45:20] TRAP v2c from 192.0.2.31:49052 (community: public)
  snmpTrapOID = IF-MIB::linkDown (1.3.6.1.6.3.1.1.5.3)
  sysUpTime = 123456
  IF-MIB::ifIndex.12 = 12
  IF-MIB::ifOperStatus.12 = down

5. MIB management

5.1. Upload

  1. The file extension must be .mib, .txt, .my, .smi or none.

  2. The file must not be empty, must not be larger than mib_max_kb and must not contain NUL bytes in the first 4 KB.

  3. The module name is read from the content (NAVN DEFINITIONS ::= BEGIN) and must match ^[A-Za-z][A-Za-z0-9-]{0,127}$.

  4. The file is written atomically as <data_dir>/mibs/source/<NAVN>. The name sent by the browser is only shown in the list and is never used as a path. Path traversal is therefore impossible.

5.2. Compilation

All uploaded modules are compiled with pysmi into pysnmp Python modules in <data_dir>/mibs/compiled/. Each module is compiled separately. A single broken file therefore does not stop the others, and each module gets its own status:

ok

Compiled and loaded

error

Message from the parser, or missing: A, B when imported modules are not found

pending

Not compiled yet

Sources for IMPORTS are searched in this order: the upload directory, the directories in mib_search_dirs (for example /usr/share/snmp/mibs if net-snmp-libs is installed) and the built-in IETF MIBs. The HTTP reader in pysmi is never used. The requests dependency is replaced by a stub that refuses downloads, so compilation only ever happens offline.

5.3. Standard MIBs

The first time a version starts, the built-in standard MIBs that define common notifications and objects are compiled once into <data_dir>/mibs/base/ (about 1.5 seconds): IF-MIB, IANAifType-MIB, HOST-RESOURCES-MIB, IP-MIB, TCP-MIB, UDP-MIB, DISMAN-EVENT-MIB, NOTIFICATION-LOG-MIB and whatever they import. Together with the modules that ship with pysnmp (SNMPv2-MIB, RFC1213-MIB, …) this means that, for example, IF-MIB::linkDown and SNMPv2-MIB::coldStart are shown by name without any upload. Uploaded modules take precedence over these.

5.4. Lookup

After each compilation a new MibViewController is built from the built-in pysnmp modules and all compiled modules, and it is swapped in under a lock. Every new trap is looked up against it. Traps that have already been received keep the names they had when they arrived.

6. Configuration

File: /etc/snmp-trap-receiver/config.ini (owner root:snmptrap, mode 0640). Restart the service after making a change.

Section Key Default Meaning

[web]

host

0.0.0.0

Address of the web GUI

port

8080

TCP port

auth

true

Require a password

password_hash

(set by the installer)

scrypt hash (Werkzeug). Set with set-password

secret_key

(random)

Signs the session cookie

default_language

en

en or no

session_hours

12

Session lifetime

[receiver]

listen

0.0.0.0:162

vert:port, comma-separated. IPv6 is written as [::]:162

autostart

true

Start reception when the service starts

communities

public

Accepted communities

accept_any_community

false

Accept all communities

buffer_size

2000

Number of traps kept in memory for the GUI

[storage]

data_dir

/var/lib/snmp-trap-receiver

MIB sources, compiled modules and status

log_dir

/var/log/snmp-trap-receiver

Trap logs

log_max_mb

20

Size per file before rotation

log_backups

10

Number of rotated files kept per log

mib_max_kb

2048

Maximum MIB upload size

mib_search_dirs

/usr/share/snmp/mibs

Additional directories with MIB sources

[v3user:NAVN]

auth_protocol

none

md5, sha, sha224, sha256, sha384, sha512

auth_key

Authentication passphrase

priv_protocol

none

des, 3des, aes128, aes192, aes256

priv_key

Privacy (encryption) passphrase

engine_id

The sender’s engine ID in hex (v3 traps)

7. Web GUI and API

7.1. Pages

Traps

Live list with the newest at the top, refreshed every 2 seconds. Free-text filter (all words must occur in the source, OID, name or value), version filter, pause, clearing the view and CSV export. Click a row to see all variables with OID, name, value and type.

MIB files

Upload one or more files. Status per module with error text, source download, deletion and recompilation.

Receiver

Running/stopped, listening addresses, addresses that failed, counters for received, rejected and errors, configured communities and v3 users, and the last error. Start, stop and restart.

Logs

All log files, including rotated ones, with size, time and download.

Footer

Version and links to Licenses (component list from third_party_inventory.json, can be filtered by license), License report and Third-party notices (with search).

The selected tab is remembered in the browser. The layout works down to mobile width. On narrow screens two of the columns in the trap list are hidden.

7.2. Endpoints

Path Method Purpose

/login, /logout

GET/POST, POST

Session

/lang/<en|no>

GET

Sets the language cookie

/

GET

Main page

/legal?doc=components|report|notices

GET

License view

/manual

GET

The user manual in the selected language, with table of contents and search. The User manual button links to /manual#<fane>

/healthz

GET

{"ok": true, "receiver": true, "version": "2.1.0"} – no login required, for monitoring

/api/status

GET

Receiver status and counters

/api/traps?since=N

GET

Traps with id > N (max. 1000)

/api/traps/clear

POST

Clears the in-memory list (the logs are not changed)

/api/receiver/<start|stop|restart>

POST

Controls the receiver

/api/mibs

GET/POST

List / upload (multipart/form-data, field mib_file, multiple allowed)

/api/mibs/compile

POST

Recompiles all

/api/mibs/<modul>/delete

POST

Deletes a module

/api/mibs/<modul>/source

GET

Downloads the source

/api/logs

GET

Lists the log files

/download/log/<fil>

GET

Downloads a log file (only the trap logs and their rotations)

/download/traps.csv

GET

CSV of the traps in memory

/api/legal, /api/legal/<report|notices>

GET

License data in the selected language

7.3. Security

  • Authentication: a single password, stored as an scrypt hash. After five failed attempts from the same address within 15 minutes, logins from that address are refused for 30 seconds. The time doubles with each further attempt, up to 15 minutes.

  • Session: signed cookie with HttpOnly and SameSite=Strict, valid for 12 hours.

  • CSRF: every POST requires the session token (the X-CSRF-Token header or a form field).

  • Headers: Content-Security-Policy: default-src 'self' with no inline script or style, X-Frame-Options: DENY, nosniff, Referrer-Policy: no-referrer and Cache-Control: no-store on the API.

  • Output: all content from traps is escaped before it is displayed. In the CSV export, cells starting with =, +, - or @ are prefixed with ', so that spreadsheets cannot execute them as formulas.

  • Upload: see Upload.

8. Language

English is the default. The default language can be changed in the configuration, and each user can switch with EN/NO in the top bar (stored in a cookie for one year). The server renders the page in the selected language, and the same dictionary is passed to JavaScript, so dynamic texts follow the language. The license report is shown in the selected language. The third-party notices are in English because they are the original license texts.

9. Installation and operation

9.1. Files and users

Path Owner, mode Contents

/opt/snmp-trap-receiver/app/

root, 0755

The application

/opt/snmp-trap-receiver/venv/

root, 0755

Python environment built from the bundled wheel files

/opt/snmp-trap-receiver/docs/

root

This documentation and the license files

/opt/snmp-trap-receiver/.previous/

root

Previous version after an upgrade

/etc/snmp-trap-receiver/config.ini

root:snmptrap, 0640

Configuration (contains the secret key and v3 keys)

/var/lib/snmp-trap-receiver/

snmptrap, 0750

MIB sources, compiled modules and status.json

/var/log/snmp-trap-receiver/

snmptrap, 0750

snmp_traps.log, snmp_traps.jsonl and rotations

/usr/local/bin/snmp-trap-receiver

root, 0755

Command line

9.2. The systemd unit

The service runs as the system user snmptrap. It is given exactly one capability, CAP_NET_BIND_SERVICE, so that it can bind UDP 162. The unit also sets NoNewPrivileges, ProtectSystem=strict (everything read-only except the data and log directories), ProtectHome, PrivateTmp, PrivateDevices, kernel and control group protection, RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX, RestrictNamespaces, LockPersonality and UMask=0027. It restarts automatically after a failure (Restart=on-failure).

SELinux needs no changes. The service runs in the unconfined_service_t domain, like other programs under /opt.

9.3. Command line

snmp-trap-receiver serve

Run the receiver and web GUI (used by systemd)

snmp-trap-receiver set-password [--password-stdin | --generate]

Set the web password (root)

snmp-trap-receiver import-mibs FILER…​

Import and compile MIB files

snmp-trap-receiver compile-mibs

Compile all uploaded MIBs. Exit code 2 if any fail

snmp-trap-receiver selftest

End-to-end test (see Verification)

snmp-trap-receiver version

Version

When run as root, all commands except set-password run as the snmptrap user, so that files get the correct owner.

9.4. The offline installer

snmp-trap-receiver-offline-installer.run is a bash script followed by a gzip-compressed tar archive. The script uses python3 to verify the archive’s SHA-256 and extracts it with the data filter in tarfile (no absolute paths, no ..). It then runs install.sh. Neither tar nor pip needs to be installed on the server.

install.sh:

  1. checks for root, the operating system, Python 3.9, the architecture against the package, glibc 2.34 or newer (required by the cryptography wheel) and the checksum of every file (SHA256SUMS);

  2. stops a running service when upgrading;

  3. creates the user and the directories;

  4. builds a new virtual environment with python3 -m venv --without-pip and installs the pinned wheel files using the bundled pip wheel (--no-index --no-deps --only-binary=:all:). pip itself is not installed in the environment;

  5. checks that the packages can be imported, swaps in the new version and moves the old one to .previous/;

  6. creates the configuration on first installation (random secret key and generated password) or keeps the existing one, and applies the options from the command line;

  7. imports MIBs, if requested;

  8. installs the systemd unit, opens ports in firewalld, restarts the service and waits for /healthz;

  9. runs the self-test.

9.5. Building the installer

On any machine with internet access:

bash build-offline-installer.sh            # x86_64 + aarch64

The script downloads the wheel files listed in Application/requirements.lock for CPython 3.9 on both architectures (manylinux 2014/2_17/2_28/2_34) and checks that every package is available for each architecture. It generates THIRD_PARTY_NOTICES.txt and third_party_inventory.json from the wheel files (including the GPL scan) and copies the application and documentation. If the build machine has Python 3.9, an offline installation and the self-test are run. pip-audit is run if it is installed. Finally the installer, a plain .tar.gz and the checksums are written to Offline-Installation/.

To update dependencies, edit Application/requirements.in, resolve the dependencies with Python 3.9 and write the result to requirements.lock.

10. Verification

snmp-trap-receiver selftest uses a free high UDP port on 127.0.0.1 and temporary directories, so it never interferes with the running service. It checks the following:

  • that cryptography is available (SNMPv3 encryption);

  • that a test MIB uploaded with the file name ../../evil/selftest.mib is stored only under its module name;

  • that the MIB compiles offline against the built-in IETF MIBs, and that a file that is not a MIB is rejected;

  • that the receiver binds. A v1 trap, a v2c trap, a v2c inform and a v3 trap with authPriv (SHA/AES) are sent, and all four are received with the correct version and type;

  • that the trap OID and the variables are resolved via the uploaded MIB, that the source address is recorded, that a wrong community is rejected and counted, and that both logs are written;

  • web: login is required, a wrong password is rejected and login works. The English page is the default, and a POST without a CSRF token is rejected. It also tests the trap API, MIB upload via the API, log download, rejection of path traversal in log download, CSV export, the user manual in English and Norwegian, the Norwegian page, security headers, the license inventory and both license documents in both languages, and stop and start via the API.

The installer runs the self-test after every installation.

11. Limitations

  • HTTP only. For access over untrusted networks, bind the GUI to 127.0.0.1 and use an SSH tunnel, or put a reverse proxy with TLS in front.

  • A single shared password. No user accounts or audit trail for actions in the GUI (start, stop, upload and delete are written to the journal with the client’s address).

  • The in-memory list holds buffer_size traps. Older traps exist only in the log files.

  • Measured capacity: around 700 traps per second on a single CPU core. Bursts of 2000 traps were received without loss. The receiver requests a 4 MB receive buffer, but the kernel caps it at net.core.rmem_max (Rocky 9 default: 208 KB, enough for a few hundred traps in one burst). Where large trap storms can occur, the limit should be raised: echo net.core.rmem_max=4194304 > /etc/sysctl.d/90-snmp-trap-receiver.conf && sysctl --system.

12. Licenses

All third-party components have permissive licenses. See Lisensrapport.md, THIRD_PARTY_NOTICES.txt and third_party_inventory.json.

Appendix A: Ports

Port

Protocol

Direction

Purpose

162

UDP

in

SNMP traps and informs (configurable)

8080

TCP

in

Web GUI (configurable)

Appendix B: Documents

Installasjonsveiledning.md

Installation, configuration, operation and upgrading

Revisjonsrapport.md

Audit of the Docker version and what has been fixed

Lisensrapport.md

Licenses and GPL scan

THIRD_PARTY_NOTICES.txt

Full license texts

third_party_inventory.json

Machine-readable component list

Endringslogg.md

Changes

Need something that sends traps to test the receiver with? Technical description for SNMP Trap Sender