SNMP-Trap-Tools(at)Work

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

SNMP Trap Sender

Simulator that sends SNMP traps and informs like a network device, with a web GUI

Version 1.1.0Revised 2026-10-02

1. Summary

SNMP Trap Sender simulates a network device that sends SNMP notifications. It can send a periodic heartbeat, simulate a reboot, take a network interface down and up, and send free-text test messages or bursts for load testing. Notifications are sent as SNMPv1 traps, SNMPv2c traps or informs, or SNMPv3 traps or informs, to one or more trap receivers.

The simulator has its own MIB, TRAPSIM-MIB. When loaded into a trap receiver, it lets the receiver show every simulated notification by name. The standard events use SNMPv2-MIB (coldStart, warmStart) and IF-MIB (linkDown, linkUp), which every receiver already knows.

It runs as a systemd service on Rocky Linux 9, either in a VM or in a Proxmox LXC container. It is controlled from a web GUI in English or Norwegian and is installed from a single file without internet access. It is the counterpart to SNMP Trap Receiver 2.0.

2. Purpose and scope

Simulates

One network device with a name, an SNMPv1 agent address, an SNMPv3 engine ID, a sysUpTime that restarts on a simulated reboot, and a heartbeat sequence number

Sends

SNMPv1 Trap-PDU, SNMPv2c Trap and InformRequest, SNMPv3 Trap and InformRequest (USM: noAuthNoPriv, authNoPriv, authPriv; MD5, SHA-1, SHA-2; DES, 3DES, AES-128/192/256) over UDP/IPv4 and UDP/IPv6

Events

Heartbeat, reboot (announced, cold/warm start, device up), interface down/up (IF-MIB and TRAPSIM-MIB) and test message

Scenarios

Reboot with downtime, flapping interface (repeated down/up) and burst at a fixed rate

Schedules

Any event repeated every N seconds, for example a heartbeat every 60 seconds

Does not

Answer SNMP GET, act as a real agent or receive traps

3. Architecture

                Rocky Linux 9 (VM or Proxmox LXC)
 ┌───────────────────────────────────────────────────────────────┐
 │  snmp-trap-sender.service  (user trapsender)                  │
 │                                                               │
 │  ┌──────────────┐  ┌────────────────────────────────────────┐ │
 │  │ Flask app on │  │ Engine: thread + asyncio event loop    │ │
 │  │ Waitress     ├─►│  send()     schedules     scenarios    │ │
 │  │ (8 threads)  │  │  one pysnmp SnmpEngine per target      │ │
 │  └──────▲───────┘  └──────────┬───────────────────┬─────────┘ │
 │         │                     │                   │           │
 │  state.json (device,          │ history (2000)    │           │
 │  targets, schedules)          │ + sent_traps.jsonl│           │
 └─────────┼─────────────────────┼───────────────────┼───────────┘
           │ HTTP 8080           │ UDP → 162         │ UDP → 162
     ┌─────┴─────┐       ┌───────┴────────┐  ┌───────┴────────┐
     │  browser  │       │ trap receiver 1│  │ trap receiver 2│
     └───────────┘       └────────────────┘  └────────────────┘

Everything runs in a single process. All SNMP traffic runs in one asyncio event loop in a separate thread. Web requests hand work to that loop with run_coroutine_threadsafe and wait for the result. Schedules and scenarios are asyncio tasks in the same loop, so they never block the GUI.

The engine has one pysnmp SnmpEngine per target. When several targets share one engine, the pysnmp high-level API also sends later notifications to the targets from earlier calls. Separate engines prevent this, and they also let a v3 target use the device’s own engine ID. An engine is rebuilt when the target or the engine ID changes.

3.1. Modules

Module Responsibility

trapsender/events.py

The event catalogue: OID per event, parameters, validation, and how each event becomes variable bindings

trapsender/engine.py

Engine: sending, schedules, scenarios, history, send log and statistics

trapsender/state.py

Simulated device, targets and schedules in state.json; validation; secrets are kept out of the GUI

trapsender/config.py

/etc/snmp-trap-sender/config.ini (web, limits, storage)

trapsender/web.py

Flask GUI and JSON API: login, CSRF and security headers

trapsender/i18n.py

GUI texts in English and Norwegian (196 keys; both languages must have the same keys)

trapsender/manual.py

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

trapsender/selftest.py

End-to-end test with a temporary local receiver

trapsender/main.py

Command line: serve, set-password, add-target, send, selftest, version

trapsender/mib/TRAPSIM-MIB.mib

The simulator’s MIB

4. Events

Every notification starts with sysUpTime.0 (the uptime of the simulated device) and snmpTrapOID.0. For SNMPv1 targets, snmpTrapAddress.0 is added when an agent address is set. pysnmp uses it as the agent-addr field in the Trap-PDU (RFC 3584), and converts the notification OID into the v1 fields enterprise, generic-trap and specific-trap.

Event Notification Variables

Heartbeat

TRAPSIM-MIB::tsimHeartbeat

tsimDeviceName, tsimSequence (1, 2, 3 … since the last reboot), tsimInterval

Reboot announced

TRAPSIM-MIB::tsimReboot

tsimDeviceName, tsimRebootReason, tsimDowntime, tsimMessage

Device up

TRAPSIM-MIB::tsimDeviceUp

tsimDeviceName, tsimRebootReason, tsimDowntime

Interface down / up

TRAPSIM-MIB::tsimInterfaceDown / tsimInterfaceUp

tsimDeviceName, tsimIfIndex, tsimIfName, tsimIfAlias, tsimSeverity, tsimMessage

Test message

TRAPSIM-MIB::tsimTestMessage

tsimDeviceName, tsimSeverity, tsimMessage

coldStart / warmStart

SNMPv2-MIB::coldStart / warmStart

–

linkDown / linkUp

IF-MIB::linkDown / linkUp

ifIndex.n, ifAdminStatus.n = up(1), ifOperStatus.n = down(2) / up(1), plus ifName.n and ifAlias.n, as many vendors do

Parameters are validated on the server: ifIndex 1…2147483647, downtime 0…86400 seconds, severity and reason from the enumerations in the MIB, interface name and description up to 64 characters and message up to 255.

5. TRAPSIM-MIB

The MIB is SMIv2 and passes smilint -l 6 (libsmi’s strictest level) without warnings. It sits under enterprise 32473, which IANA has reserved for documentation and examples (RFC 5612). It therefore never collides with the OIDs of a real vendor.

1.3.6.1.4.1.32473            example (RFC 5612)
 └─ .1                       trapSimRoot
     └─ .1                   trapSimMIB (MODULE-IDENTITY)
         ├─ .0               trapSimNotifications
         │   ├─ .1  tsimHeartbeat       .4  tsimInterfaceDown
         │   ├─ .2  tsimReboot          .5  tsimInterfaceUp
         │   ├─ .3  tsimDeviceUp        .6  tsimTestMessage
         ├─ .1               trapSimObjects (accessible-for-notify)
         │   ├─ .1 tsimDeviceName   .5 tsimRebootReason   .9  tsimIfAlias
         │   ├─ .2 tsimSequence     .6 tsimDowntime       .10 tsimMessage
         │   ├─ .3 tsimInterval     .7 tsimIfIndex
         │   └─ .4 tsimSeverity     .8 tsimIfName
         └─ .2               trapSimConformance (compliance and groups)

Enumerations: TrapSimSeverity = clear(1), info(2), warning(3), minor(4), major(5), critical(6); TrapSimRebootReason = operatorRequest(1), softwareUpgrade(2), powerFailure(3), watchdog(4), crash(5), scheduled(6).

The file is in Documentation/MIB/, in Offline-Installation/ and in the installed application (/opt/snmp-trap-sender/app/trapsender/mib/). It can also be downloaded from the Device tab.

6. Scenarios

Reboot

Sends tsimReboot (reason, downtime). The device is then “down” for the downtime: scheduled notifications pause, and the GUI shows Device rebooting. Afterwards sysUpTime and the heartbeat number start from 0 again, and the scenario sends coldStart (or warmStart) and tsimDeviceUp. This is the sequence a real device produces, so it is well suited to testing rules on the receiver side.

Flapping interface

Count times: interface down, wait down seconds, interface up, wait up seconds. The notifications are linkDown/linkUp from IF-MIB, tsimInterfaceDown/Up from TRAPSIM, or both.

Burst

Count notifications of one event at rate per second. The rate follows a fixed timetable, so one slow send does not stretch the whole burst. Limits: max_rate (default 500/s) and max_burst (default 100 000).

Scenarios send to all enabled targets, run in the background, show their progress in the GUI and can be cancelled. If a reboot is cancelled, the “down” state ends immediately.

7. Schedules

A schedule repeats one event with fixed parameters every interval seconds (1…86 400) to all enabled targets or to selected targets. The first send happens when the schedule is started. A new installation has a Heartbeat schedule that is disabled. The installer can enable it with --heartbeat N. Schedules are stored in state.json and start again together with the service.

8. Targets

A target is one trap receiver: host name or IP address (IPv4 or IPv6), UDP port, SNMP version, trap or inform, community (v1/v2c) or USM user with protocols and keys (v3), timeout and retries (used by informs), and an enabled switch.

  • Traps are sent without acknowledgement. Sent means that the packet left the sender, not that a receiver accepted it.

  • Informs are acknowledged by the receiver. If the acknowledgement is missing after the timeout and retries, it is shown as an error. Informs are the way to check that the receiver actually got the notification.

  • SNMPv3 traps are sent with the simulated device’s engine ID as the authoritative engine. The receiver must have the same user for that engine ID. The GUI shows the line to add to the configuration of SNMP Trap Receiver ([v3user:NAVN] … engine_id = …).

Communities and v3 keys are stored in state.json (mode 0600, readable only by the service user). They are never sent back to the browser. The edit form shows “unchanged” and keeps the stored value when the field is left empty.

9. Simulated device

Name

Sent as tsimDeviceName. The installer sets it to the host name.

Agent address

Optional IPv4 address placed in SNMPv1 traps as agent-addr, so that the trap appears to come from a different device

sysUpTime

Counts from the last (simulated) reboot in hundredths of a second and wraps at 232

Heartbeat number

+1 per heartbeat (the same number to all targets), back to 0 on a reboot. A receiver can spot lost heartbeats from gaps in the sequence.

Engine ID

Format per RFC 3411: 80007ed905 (enterprise 32473, octets) + 6 random bytes. Created on first start-up. New engine ID in the GUI creates a new one.

10. Web GUI and API

10.1. Pages

Send

Quick buttons (Heartbeat, Reboot, Interface down, Interface up), a form for any event with parameters and selected targets, scenarios, and running or recent scenarios with Cancel

Schedules

Table with an on/off switch per schedule; create, edit and delete

Targets

Table; create, edit, delete and Test (sends a test message to that target and shows the result)

History

The last 2000 notifications with time, notification, target, version, source (manual, schedule, scenario, CLI) and result; filter; log download

Device

Simulated device, uptime, heartbeat number, reboots, engine ID and counters; download of TRAPSIM-MIB

Top bar

Device up / Device rebooting (n s), device name and uptime, EN/NO and Log out

Footer

Version, Licenses, License report and Third-party notices

10.2. Endpoints

Path Method Purpose

/login, /logout, /lang/<en|no>

Session and language

/healthz

GET

{"ok": true, "version": "1.1.0"} without login

/manual

GET

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

/api/meta

GET

Event catalogue, enumerations and limits

/api/status

GET

Device, uptime, down state, counters and scenarios

/api/send

POST

{"event", "params", "targets"} → result per target

/api/scenario

POST

{"kind": "reboot|flap|burst", "params"}

/api/scenario/<id>/cancel

POST

Cancel

/api/targets

GET/POST

List (without secrets) / create or update

/api/targets/<id>/test, …/delete

POST

Test / delete

/api/jobs

GET/POST

Schedules

/api/jobs/<id>/enable, …/delete

POST

On/off / delete

/api/history?since=N

GET

Sent notifications with id > N

/api/device, /api/device/engine-id

POST

Device name and agent address / new engine ID

/download/mib, /download/log

GET

TRAPSIM-MIB / sent_traps.jsonl

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

GET

License data

10.3. Security

Same model as SNMP Trap Receiver: a single password (scrypt hash), session cookie with HttpOnly and SameSite=Strict, a delay after 5 failed logins, a CSRF token on every POST, Content-Security-Policy: default-src 'self' with no inline script, X-Frame-Options: DENY and nosniff. All values are escaped before they are displayed, and requests are limited to 256 KB.

The sender can be used to flood a receiver (burst). Access to the GUI should therefore be restricted to those who run tests.

11. Configuration

/etc/snmp-trap-sender/config.ini (root:trapsender, 0640). Restart the service after a change.

Section Key Default Meaning

[web]

host, port

0.0.0.0, 8080

Address of the web GUI

auth, password_hash, secret_key

true, set by the installer

Login

default_language

en

en or no

session_hours

12

[sender]

max_rate

500

Upper limit for bursts, per second

max_burst

100000

Largest burst

history_size

2000

Notifications kept for the History tab

[storage]

data_dir

/var/lib/snmp-trap-sender

state.json

log_dir

/var/log/snmp-trap-sender

sent_traps.jsonl

log_max_mb, log_backups

20, 5

Log rotation

Targets, schedules and the device are changed in the GUI and stored in state.json.

12. Log format

Each line in sent_traps.jsonl is one sent notification:

{"id": 42, "time": "2026-10-02 12:20:53", "ts": 1790936453.2,
 "event": "sim_if_down", "trap": "TRAPSIM-MIB::tsimInterfaceDown",
 "origin": "manual", "target": "Receiver", "target_id": "1cce00181e",
 "dest": "172.16.101.83:162", "version": "v2c", "notify": "trap",
 "params": {"if_index": 1, "if_name": "GigabitEthernet0/1",
            "if_alias": "Uplink", "severity": "major",
            "message": "Fibre cut on uplink"},
 "result": "sent", "error": "", "ms": 1.4}

result is sent, acknowledged (inform answered) or error.

13. Installation and operation

13.1. Files

Path

Owner, mode

Contents

/opt/snmp-trap-sender/

root

Application, Python environment and documentation; .previous/ after an upgrade

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

root:trapsender, 0640

Configuration

/var/lib/snmp-trap-sender/state.json

trapsender, 0600

Device, targets (with secrets) and schedules

/var/log/snmp-trap-sender/

trapsender, 0750

sent_traps.jsonl and rotated files

/usr/local/bin/snmp-trap-sender

root, 0755

Command line

13.2. systemd and LXC

The service runs as the system user trapsender without capabilities. Sending to port 162 needs no privileges, because the source port is a random high port. The unit itself sets only NoNewPrivileges, an empty CapabilityBoundingSet and UMask=0027, and that works in every container.

Further sandboxing is in a drop-in file, snmp-trap-sender.service.d/hardening.conf (ProtectSystem=strict, PrivateTmp, PrivateDevices, ProtectKernel*, RestrictNamespaces, RestrictAddressFamilies, …). It requires mount namespaces. These work in VMs and in Proxmox LXC containers with nesting enabled, which is the default for new unprivileged containers. Without nesting, systemd stops the service with status=226/NAMESPACE. The installer detects this, removes the drop-in file, restarts and reports it. --no-hardening skips the drop-in file from the start.

The service does not listen on any SNMP port, so only the web port needs to be open.

13.3. Command line

snmp-trap-sender serve

Run (used by systemd)

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

Web password (root)

snmp-trap-sender add-target VERT [--port N] [--version v1|v2c] [--community C] [--heartbeat N]

Add a target, then restart the service

snmp-trap-sender send HENDELSE [-p navn=verdi …] [-t MOTTAKER_ID]

Send one event now, e.g. send link_down -p if_index=3 -p if_name=Gi0/3. Events: heartbeat, sim_reboot, sim_device_up, sim_if_down, sim_if_up, test_message, cold_start, warm_start, link_down, link_up

snmp-trap-sender selftest

End-to-end test (see Verification)

snmp-trap-sender version

13.4. The offline installer

Same structure as for SNMP Trap Receiver. A bash header uses python3 to verify the SHA-256 of the archive appended at the end, extracts it with the safe data filter in tarfile and starts install.sh. That script checks the operating system, Python 3.9, the architecture, glibc 2.34 or later and the checksums of the files. It then builds a virtual environment solely from the bundled wheel files (no network), and installs the application, user, configuration and systemd unit. Optionally it adds a target and a heartbeat. Finally it starts the service, waits for /healthz and runs the self-test. An upgrade keeps the configuration, state.json and logs, and moves the previous application to .previous/.

build-offline-installer.sh builds the installer on any machine with internet access. It downloads the wheel files in Application/requirements.lock for CPython 3.9 on x86_64 and aarch64, generates the license files and the GPL scan, verifies an offline installation with Python 3.9 if the build machine has it, and runs pip-audit if it is installed.

14. Verification

snmp-trap-sender selftest starts a pysnmp receiver on a free port on 127.0.0.1 (community selftest and one v3 user with SHA-256/AES-128 for the sender’s engine ID) and uses temporary directories. It checks:

  • that state.json has mode 0600, that four targets (v1, v2c trap, v2c inform, v3 authPriv) are stored, and that an invalid host is rejected;

  • that each of the 10 events arrives via all four targets with the correct snmpTrapOID, that versions v1, v2c and v3 arrive, that informs are acknowledged, that the v1 agent address is the simulated one, that the heartbeat has the device name and number 1, and that linkDown has ifIndex, ifAdminStatus = up and ifOperStatus = down;

  • that the reboot scenario produces tsimReboot → coldStart → tsimDeviceUp and resets sysUpTime, the heartbeat number and the reboot counter; that flapping ×2 with both notification types produces 8 notifications; and that a burst of 40 at 100/s takes between 0.3 and 3 seconds and delivers all 40;

  • that a heartbeat schedule every second gives increasing numbers, that history and log are written, and that an inform to a port with no receiver gives an error;

  • web: login, wrong password, English as default, CSRF, sending via the API, rejection of invalid parameters, no secrets in the target list, that keys are kept when a target is edited, starting a scenario, MIB download, the user manual in English and Norwegian, the Norwegian page, security headers, and the license inventory and documents in both languages.

The installer runs the self-test after every installation (46 checks).

15. Limitations

  • HTTP only. Put a reverse proxy with TLS in front, or use an SSH tunnel over untrusted networks.

  • One simulated device per installation. For more devices, run several containers.

  • The simulator does not answer SNMP GET, so a receiver that polls the device after a trap gets no response.

  • Measured on one core: around 1000 v2c traps per second in a burst. The pysnmp engine is the bottleneck.

16. Licenses

All third-party components have permissive licenses. See Lisensrapport.md, THIRD_PARTY_NOTICES.txt and third_party_inventory.json. TRAPSIM-MIB is part of this project and may be freely used, copied and modified.

Appendix A: Ports

Port

Protocol

Direction

Purpose

8080

TCP

in

Web GUI (configurable)

162 (configurable per target)

UDP

out

Traps and informs to the targets

The counterpart that receives and displays the notifications: Technical description for SNMP Trap Receiver