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 |
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 |
|---|---|
|
The event catalogue: OID per event, parameters, validation, and how each event becomes variable bindings |
|
|
|
Simulated device, targets and schedules in |
|
|
|
Flask GUI and JSON API: login, CSRF and security headers |
|
GUI texts in English and Norwegian (196 keys; both languages must have the same keys) |
|
Shows the user manual ( |
|
End-to-end test with a temporary local receiver |
|
Command line: |
|
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 |
|
tsimDeviceName, tsimSequence (1, 2, 3 … since the last reboot), tsimInterval |
Reboot announced |
|
tsimDeviceName, tsimRebootReason, tsimDowntime, tsimMessage |
Device up |
|
tsimDeviceName, tsimRebootReason, tsimDowntime |
Interface down / up |
|
tsimDeviceName, tsimIfIndex, tsimIfName, tsimIfAlias, tsimSeverity, tsimMessage |
Test message |
|
tsimDeviceName, tsimSeverity, tsimMessage |
coldStart / warmStart |
|
– |
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 |
Flapping interface |
Count times: interface down, wait down seconds, interface up, wait up seconds. The notifications are |
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: |
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 |
Agent address |
Optional IPv4 address placed in SNMPv1 traps as |
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: |
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 |
|---|---|---|
|
Session and language |
|
|
GET |
|
|
GET |
The user manual in the selected language, with a table of contents and search. The User manual button links to |
|
GET |
Event catalogue, enumerations and limits |
|
GET |
Device, uptime, down state, counters and scenarios |
|
POST |
|
|
POST |
|
|
POST |
Cancel |
|
GET/POST |
List (without secrets) / create or update |
|
POST |
Test / delete |
|
GET/POST |
Schedules |
|
POST |
On/off / delete |
|
GET |
Sent notifications with |
|
POST |
Device name and agent address / new engine ID |
|
GET |
TRAPSIM-MIB / |
|
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 |
|---|---|---|---|
|
|
|
Address of the web GUI |
|
|
Login |
|
|
|
|
|
|
|
||
|
|
|
Upper limit for bursts, per second |
|
|
Largest burst |
|
|
|
Notifications kept for the History tab |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
root |
Application, Python environment and documentation; |
|
root:trapsender, 0640 |
Configuration |
|
trapsender, 0600 |
Device, targets (with secrets) and schedules |
|
trapsender, 0750 |
|
|
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
|
Run (used by systemd) |
|
Web password (root) |
|
Add a target, then restart the service |
|
Send one event now, e.g. |
|
End-to-end test (see Verification) |
|
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.jsonhas 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 thatlinkDownhas ifIndex, ifAdminStatus = up and ifOperStatus = down; -
that the reboot scenario produces
tsimReboot→coldStart→tsimDeviceUpand resetssysUpTime, 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 |