Web API
The simulator exposes a REST control-plane on port 8080 (override with
-port) for device CRUD, CSV / route-script
export, system stats, and flow-export status. The same port also serves the
management web UI at /.
Endpoint catalog
| Endpoint | Method | Description |
|---|---|---|
/api/v1/devices | POST | Create devices (bulk, round-robin, category-based). |
/api/v1/devices | GET | List all devices. |
/api/v1/devices/{id} | DELETE | Delete a specific device. |
/api/v1/devices | DELETE | Delete all devices. |
/api/v1/devices/export | GET | Export device list to CSV. |
/api/v1/devices/routes | GET | Generate a routing script (Debian/Ubuntu). |
/api/v1/resources | GET | List available device resource types. |
/api/v1/status | GET | Manager status. |
/api/v1/system-stats | GET | System stats (file descriptors, memory). |
/api/v1/version | GET | Running simulator version string. Immutable per process; response carries Cache-Control: max-age=3600. |
/api/v1/flows/status | GET | Flow export status and cumulative counters. |
/api/v1/traps/status | GET | SNMP trap export status, INFORM counters, and per-type catalog map. |
/api/v1/devices/{ip}/trap | POST | Fire a named catalog trap on a specific device. |
/api/v1/syslog/status | GET | UDP syslog export status, counters, and per-type catalog map. |
/api/v1/devices/{ip}/syslog | POST | Fire a named catalog syslog message on a specific device. |
/health | GET | Health check endpoint. |
Create devices
Bulk creation supports round-robin across all device types, category-based filtering, per-request SNMP port selection, and an optional SNMPv3 block.
# Round-robin across all device types
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "192.168.100.1",
"device_count": 10,
"netmask": "24",
"round_robin": true
}'
# Non-privileged SNMP port (avoids CAP_NET_BIND_SERVICE)
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "192.168.100.1",
"device_count": 5,
"netmask": "24",
"snmp_port": 1161
}'
# Filter by category
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "192.168.100.1",
"device_count": 3,
"netmask": "24",
"round_robin": true,
"category": "GPU Servers"
}'
# SNMPv3
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "192.168.100.1",
"device_count": 5,
"netmask": "24",
"snmpv3": {
"enabled": true,
"engine_id": "0x80001234",
"username": "admin",
"password": "authpass123",
"auth_protocol": "md5",
"priv_protocol": "aes128"
}
}'
# Per-device error-counter scenario
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "192.168.100.1",
"device_count": 3,
"netmask": "24",
"if_error_scenario": "degraded"
}'
The if_error_scenario field controls the per-device ppm bands used to
derive ifInErrors, ifOutErrors, ifInDiscards, and ifOutDiscards
from live packet counters. Accepted values: clean (default, no error
growth), typical, degraded, failing. Unknown values reject the
batch atomically with 400. REST-created devices default to clean
independently of the -if-error-scenario CLI flag — you must opt in
explicitly. See SNMP reference for
the full scenario bands and counter model.
A specific resource file can be requested directly (useful for storage devices):
# Create a Pure Storage FlashArray device
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "192.168.100.1",
"device_count": 1,
"netmask": "24",
"resource_file": "pure_storage_flasharray.json"
}'
Per-device export blocks
POST /api/v1/devices accepts three optional top-level blocks —
flow, traps, syslog — that attach export configuration to every
device created by the request. Any block can be omitted; omitted blocks
mean "this batch does not participate in that export subsystem."
The subsystems are always-on after main() — flow / trap / syslog
scheduler goroutines and catalog loaders run regardless of whether any
CLI seed was supplied, so REST-created devices can opt in to any
combination.
flow block:
"flow": {
"collector": "192.168.1.10:2055", // required; host:port
"protocol": "netflow9", // optional; "netflow9" | "ipfix" | "netflow5" | "sflow" (alias: "sflow5"); default "netflow9"
"tick_interval": "5s", // optional; global ticker used, per-device value validated and logged if divergent
"active_timeout": "30s", // optional; default 30s
"inactive_timeout": "15s" // optional; default 15s
}
No per-device override exists for source_per_device — the
-flow-source-per-device CLI flag is simulator-wide (see
CLI flags → Flow export). Setting
"source_per_device" in the REST body is rejected by
DisallowUnknownFields.
traps block:
"traps": {
"collector": "192.168.1.10:162", // required; host:port
"mode": "trap", // optional; "trap" | "inform"; default "trap"
"community": "public", // optional; SNMPv2c community; default "public"
"interval": "30s", // optional; per-device mean Poisson firing interval; default 30s
"inform_timeout": "5s", // optional; INFORM retry timeout; default 5s
"inform_retries": 2 // optional; max retransmissions per INFORM; default 2
}
INFORM mode requires the simulator-wide -trap-source-per-device=true
(the default). The check is enforced at device-attach time: if a
request sets mode: "inform" while the flag is false, the attach
fails per-device and the device's trapConfig is cleared so
ListDevices doesn't show a ghost entry. This is distinct from
request-level validation (which would fail the whole batch) — INFORM
without per-device binding is a runtime attach failure, not a 400.
syslog block:
"syslog": {
"collector": "192.168.1.10:514", // required; host:port
"format": "5424", // optional; "5424" | "3164"; default "5424"
"interval": "10s" // optional; per-device mean Poisson firing interval; default 10s
}
Durations accept Go duration strings ("10s", "5m", "1m30s");
integer seconds are rejected.
Combined example — flow + traps + syslog on the same batch:
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "10.0.0.1",
"device_count": 100,
"netmask": "24",
"flow": {
"collector": "192.168.1.10:4739",
"protocol": "ipfix"
},
"traps": {
"collector": "192.168.1.10:162",
"mode": "trap",
"community": "public"
},
"syslog": {
"collector": "192.168.1.10:514",
"format": "5424"
}
}'
Heterogeneous fleet — two batches pointing at different collectors:
# Batch A: 50 devices → collector A, 5424
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "10.0.0.1",
"device_count": 50,
"syslog": {"collector": "192.168.1.10:514", "format": "5424"}
}'
# Batch B: 20 devices → collector A, 3164 (same host, different format)
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "10.0.1.1",
"device_count": 20,
"syslog": {"collector": "192.168.1.10:514", "format": "3164"}
}'
# Batch C: 30 devices → collector B, 5424
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-d '{
"start_ip": "10.0.2.1",
"device_count": 30,
"syslog": {"collector": "192.168.1.20:514", "format": "5424"}
}'
GET /api/v1/syslog/status then reports three collector records keyed
by (collector, format). See
Syslog export status.
Validation failures return 400 with the underlying error (e.g.
unknown protocol, invalid collector address, unresolvable host,
explicitly invalid syslog format — non-5424 / non-3164); no device
from the batch is created (atomic batch failure). Unknown / typo'd JSON
fields at any level are also rejected via DisallowUnknownFields —
e.g. "interval_ms": 10000 lands as a 400, not a silent drop.
List devices
curl http://localhost:8080/api/v1/devices
Export to CSV
curl http://localhost:8080/api/v1/devices/export -o devices.csv
Generate a route script
curl http://localhost:8080/api/v1/devices/routes -o add_routes.sh
The generated script adds Linux kernel routes for every device IP — handy when running the simulator inside a VM and testing from the host.
Delete devices
# Single device
curl -X DELETE http://localhost:8080/api/v1/devices/{device-id}
# All devices
curl -X DELETE http://localhost:8080/api/v1/devices
Version
Report the running simulator's version. The value is baked into the binary
at build time via the Makefile's APP_VERSION variable (resolution order:
APP_VERSION env > git describe --tags > dev) and passed to go build
as -ldflags "-X main.Version=…". It never changes for the lifetime of the
process, so the endpoint sets Cache-Control: max-age=3600 — reloads of
the web UI within a browser session will reuse the cached value.
Release binaries report the clean tag (e.g., v0.5.0). A make build
from a HEAD that is ahead of the last tag reports the commit-distance
form (e.g., v0.4.1-11-g0356c42), so a post-release dev binary never
masquerades as the tagged release.
curl http://localhost:8080/api/v1/version
{"version": "v0.5.0"}
For an untagged development build (or any build produced by go build
directly, bypassing the Makefile), the reported version is the literal
string dev. Operators troubleshooting a version mismatch can call the
same string from the CLI without starting the server:
./nl6 -version
# → v0.5.0
Flow export status
curl http://localhost:8080/api/v1/flows/status
When flow export is enabled:
{
"success": true,
"message": "Success",
"data": {
"subsystem_active": true,
"collectors": [
{"collector": "192.168.1.10:4739", "protocol": "ipfix", "devices": 50, "sent_packets": 8123, "sent_bytes": 12123456, "sent_records": 243690},
{"collector": "192.168.1.20:6343", "protocol": "sflow", "devices": 20, "sent_packets": 3100, "sent_bytes": 5560000, "sent_records": 62000}
],
"devices_exporting": 70,
"last_template_send": "2026-04-23T10:35:00Z"
}
}
Response fields:
| Field | Meaning |
|---|---|
subsystem_active | true after main() boots the flow ticker goroutine — always-on. Not reachable as false via the HTTP endpoint during normal operation: the subsystem initialises with the rest of the process and only stops at process exit, alongside the HTTP server itself. |
collectors[] | One record per (collector, protocol) tuple that ever had a device. Deleted-device counters persist in the aggregate until process exit. |
collectors[].devices | Count of LIVE exporters for this tuple. 0 means no live device but the aggregate remembers prior fires. |
collectors[].sent_packets / sent_bytes / sent_records | Cumulative across live + historical exporters for this tuple (monotonic within subsystem lifecycle). |
devices_exporting | Total LIVE exporters across all tuples. |
last_template_send | ISO-8601 timestamp of the most recent template emission (NetFlow v9 / IPFIX only). |
Clients detect "no flow export configured" via len(collectors) == 0.
The retired scalar fields (enabled, protocol, collector,
total_flows_exported, total_packets_sent, total_bytes_sent) were
removed in phase 3; callers that depended on them must migrate to the
array-of-collectors shape.
See Flow export (operator guide) and Flow export reference for protocol-specific details.
Trap export status
curl http://localhost:8080/api/v1/traps/status
Unlike the flow-status endpoint, this response is not wrapped in the
{success, message, data} envelope — the handler serialises TrapStatus
directly.
{
"subsystem_active": true,
"collectors": [
{
"collector": "192.168.1.10:162",
"mode": "inform",
"devices": 80,
"sent": 182430,
"informs_pending": 17,
"informs_acked": 182380,
"informs_failed": 33,
"informs_dropped": 0
},
{
"collector": "192.168.1.20:162",
"mode": "trap",
"devices": 20,
"sent": 6000
}
],
"devices_exporting": 100,
"rate_limiter_tokens_available": 94,
"catalogs_by_type": {
"_universal": {"entries": 5, "source": "embedded"},
"cisco_ios": {"entries": 12, "source": "file:resources/cisco_ios/traps.json"},
"juniper_mx240": {"entries": 12, "source": "file:resources/juniper_mx240/traps.json"}
}
}
The four informs_* fields only appear on records whose mode == inform.
TRAP-mode records omit them.
subsystem_active is the authoritative feature-on signal — true
after StartTrapSubsystem runs. In normal operation, the HTTP
endpoint always returns true: the subsystem initialises from main()
and the only path that sets subsystem_active=false is StopTrapExport,
which is invoked at process shutdown alongside the HTTP server. A
false value is therefore only observable programmatically (e.g.
from a test harness calling GetTrapStatus without starting the
subsystem). Clients that previously branched on the retired enabled
scalar should use subsystem_active. len(collectors) == 0 with
subsystem_active=true means the subsystem is running but no device
has opted in.
catalogs_by_type keys are device-type slugs (plus the reserved
_universal entry for the fallback catalog). source is
"embedded", "file:<path>", or "override:<path>" when
-trap-catalog was supplied. When disabled:
{"subsystem_active": false, "collectors": [], "devices_exporting": 0}
rate_limiter_tokens_available is only present when -trap-global-cap is
set. The sent counter increments on every wire emission including
INFORM retransmissions, so it can exceed `informs_acked + informs_failed
- informs_dropped + informs_pending` under retry churn.
Counters are monotonic within a subsystem lifecycle: deleting a
device does not zero its collector's sent; the aggregate survives.
See SNMP trap / INFORM export (operator guide) and SNMP trap reference for the full feature details.
Fire a trap on demand
curl -X POST http://localhost:8080/api/v1/devices/192.168.100.1/trap \
-H "Content-Type: application/json" \
-d '{"name":"linkDown","varbindOverrides":{"IfIndex":"3"}}'
Request body:
| Field | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | Catalog entry name (e.g. linkDown, ciscoConfigManEvent). Must match an entry in the device's resolved catalog (per-type overlay if present, universal otherwise) — not the universal catalog globally. |
varbindOverrides | object | no | Map of template-field → string-value overrides. Only fields from the nine-field unified vocabulary are accepted (IfIndex, IfName, Uptime, Now, DeviceIP, SysName, Model, Serial, ChassisID). |
Response:
| Status | Body | When |
|---|---|---|
202 Accepted | {"requestId": <uint32>} | Trap has been enqueued. In INFORM mode the requestId is the INFORM PDU's request-id. |
400 Bad Request | {"error": "...", "catalog": "<slug>", "availableEntries": [...]} | Unknown catalog entry for this device. The enriched body reports which catalog the device resolved to (cisco_ios, _universal, etc.) and lists its entries alphabetically so a scripted caller can fix its call without a separate discovery endpoint. For malformed JSON or missing name, the legacy envelope form {"success": false, "message": "..."} applies. |
404 Not Found | error JSON | Unknown device IP. |
500 Internal Server Error | error JSON | Template resolve error, catalog resolution returned nil despite feature active (pathological manager state), or write failure. |
503 Service Unavailable | error JSON | The trap subsystem has not started or the target device has no trap config. |
The endpoint does not block waiting for an INFORM ack — use
/api/v1/traps/status to observe INFORM lifecycle counters.
Syslog export status
curl http://localhost:8080/api/v1/syslog/status
When syslog export is enabled:
{
"subsystem_active": true,
"collectors": [
{"collector": "192.168.1.10:514", "format": "5424", "devices": 50, "sent": 18240, "send_failures": 3},
{"collector": "192.168.1.10:514", "format": "3164", "devices": 20, "sent": 6130, "send_failures": 0}
],
"devices_exporting": 70,
"rate_limiter_tokens_available": 380,
"catalogs_by_type": {
"_universal": {"entries": 6, "source": "embedded"},
"cisco_ios": {"entries": 14, "source": "file:resources/cisco_ios/syslog.json"},
"juniper_mx240": {"entries": 13, "source": "file:resources/juniper_mx240/syslog.json"}
}
}
Tuples are keyed by (collector, format): a single collector receiving
5424 from some devices and 3164 from others surfaces as two separate
records. Per-device bind failures are non-fatal — the exporter falls
back to the shared-pool socket with a warning and the sent counter
still increments.
subsystem_active has the same semantics as on the trap status
endpoint; len(collectors) == 0 is not sufficient on its own to
imply "feature off." When disabled:
{"subsystem_active": false, "collectors": [], "devices_exporting": 0}
format is "5424" or "3164". catalogs_by_type follows the same
shape as the trap endpoint. rate_limiter_tokens_available is present
only when -syslog-global-cap is set. When disabled the response is
{"enabled": false}.
See UDP syslog export (operator guide) and UDP syslog reference for the full feature details.
Fire a syslog message on demand
curl -X POST http://localhost:8080/api/v1/devices/192.168.100.1/syslog \
-H "Content-Type: application/json" \
-d '{"name":"interface-down","templateOverrides":{"IfIndex":"3"}}'
Request body:
| Field | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | Catalog entry name. Same device's-catalog resolution rule as the trap endpoint. |
templateOverrides | object | no | Nine-field unified vocabulary (same set as varbindOverrides on the trap side). |
Response:
| Status | Body | When |
|---|---|---|
202 Accepted | {} | Message emitted. On-demand fires do not consume global rate-cap tokens. |
400 Bad Request | {"error": "...", "catalog": "<slug>", "availableEntries": [...]} | Unknown catalog entry for this device. Same enriched-error shape as the trap endpoint. |
404 Not Found | error JSON | Unknown device IP. |
500 Internal Server Error | error JSON | Pathological catalog-resolution-nil state. |
503 Service Unavailable | error JSON | The syslog subsystem has not started or the target device has no syslog config. |
Device interaction
The control-plane only manages devices — once a device is up, you interact with it via its own IP on port 22 (SSH), 161 (SNMP), and, for storage devices, 8443 (HTTPS).
# SSH (VT100 terminal emulation)
ssh simadmin@192.168.100.1 # password: simadmin
# SNMP v2c
snmpget -v2c -c public 192.168.100.1 1.3.6.1.2.1.1.1.0
snmpwalk -v2c -c public 192.168.100.1 1.3.6.1.2.1.2.2.1
# SNMP v3 (when enabled)
snmpget -v3 -l authPriv -u admin -a MD5 -A authpass123 -x AES -X privpass123 \
-e 0x80001234 192.168.100.1 1.3.6.1.2.1.1.1.0
See SNMP reference for the OID coverage, including the dynamic HC
interface counters on ifXTable.
Storage HTTPS endpoints
Storage devices expose vendor-shaped REST APIs on port 8443 with shared TLS certificates generated at simulator startup.
# Pure Storage FlashArray
curl -k https://192.168.100.1:8443/api/2.14/volumes
curl -k https://192.168.100.1:8443/api/2.14/arrays
curl -k https://192.168.100.1:8443/api/2.14/arrays/space
# NetApp ONTAP
curl -k https://192.168.100.1:8443/api/cluster
curl -k https://192.168.100.1:8443/api/storage/volumes
curl -k https://192.168.100.1:8443/api/storage/aggregates
# AWS S3
curl http://192.168.100.1:8443/ # list buckets
curl http://192.168.100.1:8443/my-bucket # bucket contents