What it is
Servienta gives your test suite a controlled instance of the network services your application talks to — a file server on five transports and receivers for syslog, SNMP traps, RADIUS, TACACS+, DNS, NTP, Kafka, and IPFIX. Bring it up with one command, generate traffic, read back exactly what arrived, inject faults, and reset — reproducibly, offline.
The application under test is out of scope: Servienta never reaches into it. It only accepts, serves, and reports what it observed.
Core concepts
Quickstart
Run Servienta, serve a file, record a message, read it back, reset. Free image, no license.
docker pull ghcr.io/servienta/servienta:latest mkdir -p fixtures && echo hello > fixtures/hello.txt docker run --rm -p 5000:5000 -p 5001:5001 -p 8080:8080 -p 9000:9000 \ -v "$PWD/fixtures:/fixtures:ro" ghcr.io/servienta/servienta:latest # discover endpoints, serve a fixture byte-for-byte curl http://localhost:5001/api/v1/endpoints curl http://localhost:8080/hello.txt # declare a run, send traffic, read it back curl -X PUT http://localhost:5001/api/v1/runs/run-1 -d '{"sources":["172.17.0.1"]}' echo 'hello from my app' | nc localhost 9000 curl 'http://localhost:5001/api/v1/received/reference?run=run-1' # reset to a known state curl -X POST http://localhost:5001/api/v1/reset
Your app can't put a run id inside a raw syslog packet, so a run claims the source addresses its traffic comes from. Servienta attributes each message to a run by source; one source belongs to one active run.
The fixture tree is your host directory, mounted read-only — folders map to URL paths, edits are visible immediately, Servienta serves bytes as they are and never writes into it. Air-gapped host? Move the image as a file: docker save → docker load. Two instances on one machine? In-container ports are fixed, host ports are yours — remap the second one.
Receivers
Send to a service, read it back parsed at /api/v1/received/<service>?run=<id>.
Response control
Steer what RADIUS, TACACS+, DNS, and NTP answer, for the run. Default is a successful reply; reset restores it.
curl -X PUT .../api/v1/responses/radius -d '{"outcome":"reject","reason":"locked"}'
curl -X PUT .../api/v1/responses/dns -d '{"outcome":"nxdomain"}'
curl -X PUT .../api/v1/responses/ntp -d '{"outcome":"stratum","stratum":5}'
curl -X PUT .../api/v1/responses/dns -d '{}' # restore default
Fault injection
Force failures on both sides to test your error paths. Reset lifts them.
# file faults: auth-reject | missing | truncate | corrupt curl -X PUT .../api/v1/faults/files/hello.txt -d '{"kind":"truncate"}' # receiver modes: refuse | drop | delay | cut | error curl -X PUT .../api/v1/faults/receivers/syslog -d '{"mode":"drop"}'
Licensing
A license enables a set of stands. It is an Ed25519-signed file, validated at startup, fully offline. The free image runs the file server and a demo receiver; the service receivers unlock with a license — Files, Standard, Enterprise, or a custom stand set.
# mount a license and start curl -X PUT http://localhost:5001/api/v1/license -d @license.json # or paste it in the console: License section -> Apply a license
Offline licensing of self-hosted software is compliance clarity, not DRM: it makes what is enabled signed and verifiable, and prevents forgery. Every shipped credential is throwaway and documented as such.
API reference
Base path /api/v1 on the control port (default 8080). Full schema ships as openapi.yaml.