Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog

All notable changes to sipr are documented here. The format follows Keep a Changelog, and the project adheres to Semantic Versioning.

Unreleased

0.33.1 — 2026-10-02

Fixed

  • The library crates on crates.io (sipr-auth, sipr-control, sipr-engine, sipr-media, sipr-net, sipr-scenario, sipr-stats, sipr-tui) now carry a README; their crates.io pages were blank.

0.33.0 — 2026-09-30

Changed

  • Less CPU on both sides of a call, about a quarter less on Linux at 2000–5000 cps (docs/PERFORMANCE.md), and sipr’s UAS is now cheaper than sipp’s at every measured rate, where it cost about 20% more. The gap was kernel time, not sipr’s code: the timer thread was woken on every timer armed or cancelled, and a relay thread re-sent every inbound message into the engine’s channel. The timer thread is now woken only for a timer due before the one it sleeps toward, and messages reach the engine’s loop directly.
  • The pacer ticks about five calls’ worth of time (1–20 ms, sized to the rate) instead of every 20 ms, so -r 5000 no longer starts 100 calls back to back. Those bursts overflowed the peer’s socket receive buffer into failed calls, the more so the faster sipr starts calls. SIPp’s generator opens about as many per millisecond.
  • sipr-net: transports deliver into a NetSink. A Sender<NetEvent> converts into one, so existing callers compile unchanged; NetSink::wrapping delivers into a channel of the caller’s own events. TimerService::cancel no longer wakes the timer thread.

0.32.0 — 2026-09-30

Added

  • --sipr-ws-path and --sipr-ws-origin (M53): the resource on the WebSocket upgrade’s request line (default /) and an Origin header, for gateways that route or gate on them. Both are validated at start-up and warned about under a transport other than -t ws1|wsn|wss1|wssn. In the library, EngineConfig::ws_request holds a WsRequest, and server_handshake now returns a ServerUpgrade naming the path and Origin a client asked for.

0.31.0 — 2026-09-28

Changed

  • The RTP echo check is judged at SIPp’s defaults (M52): every rtp_stream that sent a packet is held to -audiotolerance / -videotolerance, default 1.0, so a stream whose every check missed fails the run with exit 253 as it does under SIPp. A run against a peer that does not echo now exits 253 where it used to exit 0. sipr judged only when a tolerance flag was given (the M18 divergence, withdrawn). EngineConfig::audio_tolerance / video_tolerance are f64 now, defaulting to DEFAULT_RTP_TOLERANCE.

Added

  • -rtpcheck_debug (M52) writes the RTP check’s trace to SIPp’s debugafile (audio) and debugvfile (video) in the working directory: one line per send (hex, header included), per datagram read back, and per verdict with the running failed/sent tally, plus the stream’s tally when it ends. The flag was accepted and ignored before; -srtpcheck_debug still is.

0.30.0 — 2026-09-28

Added

  • SIP over WebSocket (M51, RFC 7118; a sipr addition, SIPp has none): -t ws1|wsn on TCP and -t wss1|wssn on TLS, one connection per peer or per call like t1|tn|l1|ln, with the same reconnection, -rsa and -max_socket behavior; [transport] renders WS/WSS. sipr is the WebSocket client on a connection it dials and the server on one it accepts, masks accordingly, answers pings and closes, and drops a peer whose upgrade or frames break RFC 6455 without ending the run (docs/SIPP_COMPAT.md §6).
  • Fuzz targets for every parser that reads untrusted bytes (M50): a cargo-fuzz package under fuzz/ with nine libFuzzer targets (the SIP message parser, the SDP scan, the scenario compiler, the keyword tokenizer, the <ereg> regex engine, injection files, pcap/pcapng, the control socket and JSON, digest/AKA auth), seed corpora, make fuzz, and a CI job that runs each target for 30 s on every push (docs/TESTING.md §6).

Fixed

  • <timewait> takes what a <pause> takes, as SIPp reads both in one branch: the common attributes — so <timewait counter=…> counts, and its rtd=, crlf, hide, display and condexec apply — and a pause’s duration (variable=, a distribution, or -d when bare). next= and ontimeout= on it are compile errors, as SIPp refuses them. sipr used to accept only milliseconds there and warn the rest away as unknown (the item left open in M48).
  • A pause’s, nop’s, sendCmd’s, recvCmd’s and timewait’s start_rtd= and rtd= are booked on entry, where SIPp’s do_bookkeeping runs them with the step’s counter; sipr applied RTDs only on a send and a matched recv, so an RTD that started or stopped on any other step measured nothing.
  • -trace_rtt rows carry milliseconds, as their column names say and as real sipp writes them: its clock counts microseconds since the process started and computeRtt divides by 1000. sipr divided once more and wrote seconds, in both columns. Found when the counters interop test started reading real sipp’s rtt file.
  • verifyauth no longer panics on an Authorization header whose algorithm= value carries a non-UTF-8 byte in its first three: the MD5/SHA-256 prefix check compared a str slice at a fixed byte index, which split the replacement character the lossy decode had put there. Found by the new auth fuzz target on its fourth input.
  • A PRINTF= injection field whose %d width or precision runs into the exabytes no longer aborts the process: the padding stops at SIPp’s message-buffer size, as SIPp’s own expansion does. Found by the injection_csv fuzz target.

0.29.0 — 2026-09-27

Added

  • The library API is documented and promised (M49 D7/D8): docs/LIBRARY_API.md is the reference for sipr-engine in a Rust harness, the crate documentation lists the supported surface, and crates/sipr-engine/examples/embed.rs runs a UAS and a UAC in one process (cargo run -p sipr-engine --example embed). Within a 0.x minor series that surface only grows; a minor bump may change it and this file says how to migrate.
  • sipr_scenario::compile_strict(name, xml, &options): the binary’s --check policy as a function — the lints run, and any diagnostic, a warning included, is an Err carrying every diagnostic.
  • sipr_engine::Run, the library entry point (M49 D2, docs/LIBRARY_API.md): Run::start(scenario, config) (or start_with for a secondary scenario) runs the engine on its own thread and returns once its sockets are bound, so a port in use is an error there; control() is an EngineControl with the new pause, resume, stop, abort and key next to set_rate; snapshots() is the engine’s once-a-second statistics as a channel; local_addr() is the bound signaling address, for the port-0 case a test needs; and wait() joins the thread and returns the RunReport. Dropping a Run without waiting aborts it and joins. run(&scenario, &config) stays as the blocking shorthand. The binary runs its engine this way now, its TUI and stdin key readers feeding EngineControl::key.
  • EngineError::Fatal: the engine thread could not be started, or ended without a report.

Fixed

  • Call numbers — [call_number], the %u of a default Call-ID, the auto media port — come from one counter for the whole process, as SIPp’s do for its one engine. Two engines in one process (a harness’s UAS and its UAC, or one run after another) used to number from 1 each and so produce the same default Call-ID, which the peer took for a late message of a call it had just finished and dropped, leaving the new call to time out. The binary, one engine per process, is unaffected.
  • A dropped transport frees its port at once. Every listener’s thread — the UDP receive loop, the TCP, TLS and SCTP accept loops, SIPp’s UDP control socket and the HTTP API’s accept loop — used to block in the kernel holding its socket until the process ended, so a run’s ports stayed bound after the run. Each now owns its thread: dropping the transport raises a flag, wakes the thread with a datagram or a connection of its own, and joins it. An embedder can start a UAS on a port, wait for it, and start another on the same port.

Changed

  • The message renderer (RenderCtx, VarCtx, render and friends) and the blocking run_with_* entry points are hidden from sipr-engine’s documentation: reachable for the bench and earlier callers, not part of the supported surface, free to change.
  • EngineConfig::inf and rxinf (formerly inf_files and rx_inf_files) hold InjectionSources: a Path read at start-up, or Text { name, csv } already in memory, so a harness can feed [fieldN] from a string. The name — the path’s basename, or the given one — is what [fieldN file=NAME] and -infindex NAME refer to, as before (M49 D5).
  • run_with_control, run_with_ui and run_scenarios are hidden from the documentation and kept for one minor release; UiChannels::snapshots is a bounded SyncSender, and a reader that falls behind misses snapshots instead of growing a queue or stalling the engine.
  • sipr_engine::EngineError is an enum — Config, Scenario, Bind (with the OS error as its source()) and Io (with the path and the OS error) — that implements std::error::Error, so ? into anyhow or Box<dyn Error> works. Every message reads exactly as before, and the enum is #[non_exhaustive] (M49 D4). One wording changed: a master that cannot dial its extended-3PCC peers now says “cannot connect the extended 3PCC peers: ” instead of the bare OS error.
  • The engine no longer prints. Everything it had to say on stderr — the bound address, the control-socket and HTTP banners, every warning: and error: line, the -bg statistics line — is a sipr_engine::Notice sent where EngineConfig::notices points: stderr with the same wording by default, a channel for an embedder, or nowhere (NoticeSink::Discard, an explicit choice). sipr-engine denies clippy’s print_stderr/print_stdout so it stays that way (M49 D3). -nostdin is the binary’s business now: the stdin key reader (q, Q, the rate keys) lives in the binary and is attached in headless mode only — a TUI run reads keys through the screen, where the engine’s own reader used to compete with it for stdin. UiChannels takes each half as an Option, so a key producer needs no snapshot reader.
  • sipr_engine::EngineConfig has Default (SIPp’s defaults) and the constructors EngineConfig::uac(target) and EngineConfig::uas(), and is #[non_exhaustive], as is RunReport: build one from a constructor and assign the fields to change. A struct literal outside the crate no longer compiles, and a field added later no longer breaks an embedder (M49 D1, docs/LIBRARY_API.md). The binary builds its configuration the same way, and a unit test keeps a bare command line equal to the engine’s defaults field for field. TlsConfig and LogOverwrite gained PartialEq.

0.28.0 — 2026-09-26

Added

  • Generic counters at parity (M48): counter= on a message is a scenario-wide statistic, as in SIPp. The statistics screen (and -trace_screen) shows a Counter <name> row per counter with its periodic and cumulative values. -trace_stat writes a <name>(P) and <name>(C) column pair per counter after CallLengthStDev, named GenericCounter<name> for an all-digit name and cut to SIPp’s 19 characters; the header matches real sipp’s byte for byte. /stats and --sipr-stats-json carry a counters list, and /metrics a sipr_scenario_counter_total{counter="…"} series each. A counter is booked where SIPp books it: a nop and a recvCmd count before their actions run, so a <jump> no longer skips the count, and a send counts even when sending fails. An empty counter name, or one containing $ or ,, is now a compile error, since SIPp refuses to load such a scenario. sipr used to keep a per-call tally that nothing reported.
  • Scenario lints under --check (M47): optional-window (an optional recv that no mandatory recv follows, which SIPp refuses to load before a send or pause and which hangs the call at the end of a scenario), unreachable (steps no path reaches, including anything after <timewait>), body-separator (SDP lines among the headers) and content-length (a literal Content-Length that is wrong, or cannot be right because the body holds keywords, or none on a message with a body). Findings print as warning[NAME], and <!-- sipr-lint: allow NAME --> right before a step silences one for that step. See docs/LINTS.md.
  • Structured statistics (M46): --sipr-stats-json FILE appends one JSON object per snapshot tick (the /stats document), flushed every second so it can be tailed live. The file is truncated at start-up, an unwritable path fails start-up, and the last line is the run’s final state. GET /metrics on the HTTP API serves the same snapshot as Prometheus text exposition: names prefixed sipr_, counters ending in _total, durations in seconds, and labels for failure reason, message kind and step. It is guarded by the bearer token like every path but /health. See docs/CONTROL_API.md §3.
  • Performance measurement (M45): make bench-vs-sipp runs sipr and real sipp against themselves on loopback at 500, 2000 and 5000 calls/s and compares CPU, peak RSS, retransmissions and concurrency. Criterion benches cover the hot path (template fill, inbound parse and route, timer churn) with baselines in benches/BASELINES.md, and a nightly Linux workflow runs both. The results, including where sipp is still faster, are in docs/PERFORMANCE.md.
  • PRINTF= injection files (M44): a PRINTF=<n> header, with optional PRINTFOFFSET= and PRINTFMULTIPLE=, turns the data rows into templates over n virtual lines, so one row can stand in for thousands of users. -infindex and -users range over the virtual lines; insert/replace are refused on such a file, as in SIPp.
  • <rtp_echo variable="v"> switches echoing from a variable (M44). SIPp parses this form but reads the wrong slot, so it always switches echoing off there; sipr reads the variable (SIPP_COMPAT §6).
  • Socket options (M44): -bind_local (listen on the -i address only), -buff_size (send and receive buffers), -sendbuffer_warn (a default, non-scenario message that cannot be sent ends the run; without the flag it warns), and -bind_to_device on Linux (SO_BINDTODEVICE, needs root; a usage error on other systems).
  • play_pcap_audio/_video/_image read pcapng captures as well as classic pcap (M44). tcpdump and Wireshark write pcapng by default, and SIPp rejects it.
  • Extended 3PCC (M43): -master NAME/-slave NAME with -slave_cfg FILE (name;host:port lines), sendCmd dest= routed to the named peer and recvCmd src= checked against the command’s From: line, on SIPp’s wiring (every instance listens on its table address; the master dials its dest= peers at start-up, a slave dials back on first contact). Twin commands are now routed by their Call-ID like SIP messages, and the 3PCC server sides (classic controller B, every slave) open their calls on the commands that name them; an optional recvCmd lets a SIP message for the recv behind it pass; a closed twin connection ends the run at once with SIPp’s warning, aborting the calls still open; internal-cmd: abort_call is honored, and a classic controller sends it when it aborts a call on an unexpected message.
  • A documentation site at https://tareqmy.github.io/sipr/, built with mdBook from docs/ and deployed from master by the new Docs workflow.
  • Timer and behavior knobs at parity (M42): -max_invite_retrans, -max_non_invite_retrans (SIPp’s 5 and 9, with -max_retrans as a ceiling; an INVITE’s timer keeps doubling past T2 as SIPp’s does), -recv_timeout, -timeout_error, global -lost, -pause_msg_ign, -default_behaviors (with -nd as none) including SIPp’s abort messages (ACK/BYE/CANCEL from its own built-in templates) and its handling of unexpected BYE, CANCEL and PING, -callid_slash_ign, -sleep, -nostdin; -send_timeout and -timer_resol are accepted with a warning. The [last_Request_URI] keyword.
  • Message and error logs at parity (M41): -trace_msg frames and -trace_err lines take SIPp’s exact shapes (timestamps, the The following events occurred: header), <log> actions go to the new -trace_logs/-log_file, and -trace_shortmsg/-shortmessage_file, -trace_calldebug/-calldebug_file, -error_file, -message_file, the -<kind>_overwrite flags, -ringbuffer_files/-ringbuffer_size/ -max_log_size rotation and -deadcall_wait are implemented as SIPp’s; -trace_timeout is accepted (a no-op in SIPp too). trace logs|shortmessages on|off work on the control socket.
  • Statistics files at parity (M40): -trace_stat writes SIPp’s full column set (StartTime … WatchdogMinor, ResponseTime<rtd> mean and standard deviation per RTD, CallLength, a repartition block per RTD and for the call length, SIPp’s time formats and trailing delimiter); new -trace_rtt/-rtt_freq, -trace_counts, -trace_error_codes, -trace_screen/-screen_file, -stat_delimiter, -periodic_rtd and -f, all with SIPp’s file names and formats.
  • Keyword parity (M39): [clock_tick], [timestamp], [date], [sipp_version], [dynamic_id], [remote_host], [tdmmap], [last_message], [last_cseq_number] (with +N/-N), [fill variable= text=] and [file name=] render as in SIPp, and -key KEYWORD VALUE defines generic keywords. New flags -tdmmap, -dynamicStart/-dynamicMax/-dynamicStep and -rfc3339. [timestamp] is UTC where SIPp uses local time (SIPP_COMPAT §6).
  • Statistical pauses at SIPp parity: <pause distribution="…"> now takes SIPp’s attribute form (distribution="normal" mean="…" stdev="…") and all ten of SIPp’s kinds — fixed, uniform, normal, lognormal, exponential, weibull, pareto, gpareto, gamma, negbin — plus the old-style <pause min= max=> spellings and SIPp’s sanity_check. The <sample assign_to= distribution=…> action draws into a variable. A pause sample below 1 ms is no pause, as in SIPp. The interop CI build of sipp now includes GSL so the comparison runs both ways.

Changed

  • --check fails on the new lint findings like any other warning, so a scenario that passed it before may now exit 1. Normal runs do not lint.
  • SIPp’s -watchdog_*, -max_recv_loops, -max_sched_loops, -rtp_threadtasks, -skip_rlimit, -plugin and its six SCTP socket options are accepted with a “has no effect in sipr” warning each, instead of being rejected (the SCTP ones were a hard error), so wrapper scripts written for sipp keep running (M44, SIPP_COMPAT §3.1).
  • A default (non-scenario) message that cannot be sent now warns, where it used to be dropped silently; -sendbuffer_warn makes it end the run (M44).
  • A 3PCC twin command must carry the call’s Call-ID: (SIPp routes by it); sipr used to hand a command to whichever call was waiting. Classic controller B no longer paces its calls with -r: as in SIPp they open when controller A’s command arrives.
  • Non-INVITE messages retransmit up to 9 times by default (SIPp’s -max_non_invite_retrans), not 5; an aborted client call now sends SIPp’s BYE/CANCEL/ACK unless -nd or -default_behaviors …,-bye; a /// prefix in an inbound Call-ID is stripped as SIPp’s 3PCC marker unless -callid_slash_ign.
  • The -trace_msg frame no longer carries the peer address, and -trace_err lines are timestamped: both are now SIPp’s formats. <log> lines no longer go to the error trace with a [log] prefix; they need -trace_logs.
  • -fd defaults to 60 s as in SIPp (it was 1 s); the final statistics row is still written at exit. The -trace_stat header changed from sipr’s earlier subset to SIPp’s columns, so parsers keyed on column position need SIPp’s layout.
  • --check and the scenario screen label a distributed pause the way SIPp’s screen does (N(60000.000,15000.000), Exp(…), Wb(…), …).
  • The scenario-side positional form distribution="uniform(200,3000)" still parses but is documented as a sipr extension; SIPp’s attributes are canonical. poisson, which SIPp never had, is now an error.

Fixed

  • The HTTP control API’s reply to POST /quit arrives in full. A soft quit with little to drain could end sipr between the reply’s head and body, so a client read a 202 with an empty body. Each reply now goes out in one write, and sipr waits up to a second, as it exits, for replies still being written.
  • A retransmitted message gets the send that answered it (the first send after it), not simply the last one sent. For one, a retransmitted 200 gets its ACK again even after a BYE has gone out. It counts on the recv’s -trace_counts _Retrans column, and the resend on the send’s. A recv’s lost= (or -lost) is rolled on it, as in SIPp. A copy of a message nothing answered yet is counted and dropped. sipr used to resend whatever it sent last, and counted retransmissions only in total. Unlike SIPp, sipr keeps answering a copy after later sends instead of failing the call on it.
  • A <sendCmd> takes <action>, as in SIPp, and runs it once the command is sent. A <jump> there lands as a nop’s does. sipr refused the scenario (“unexpected inside ”).
  • With packet loss on (-lost, or any lost=), each simulated loss counts on its step, and -trace_counts gives every send and recv SIPp’s _Lost column. The scenario screen shows a lost column. sipr dropped the messages but counted nothing, so its counts header was shorter than sipp’s.
  • The +N/-N keyword offsets SIPp documents work: [cseq+1], [len+3], [remote_port+3] and [local_port-1] add theirs. An offset on a keyword SIPp ignores it on, such as [call_number+1], renders the keyword as SIPp does, with a warning that it has no effect. sipr sent all of these through verbatim with an unknown-keyword warning. Behavior change: [len] renders as SIPp’s %5u, right-aligned in five columns (Content-Length: 34), and a message without a body gets 0.
  • A <pause>’s and a <timewait>’s actions run, as in SIPp: when the pause starts, with a <jump> taking effect once it is over. A jump from a timewait sends the call on instead of ending it. sipr dropped every child of both elements without a word. A child other than <action> is now an error, as on a <nop>. A timewait now counts a Pause_Sessions entry in -trace_counts, as a pause does.
  • A false bool, such as a failed <test> result, renders as false in [$var], as real sipp writes it. sipr rendered it empty, having misread SIPp’s rendering code for a true-or-nothing rule.
  • A variable reads as a number the way SIPp’s getDouble reads it: a double’s value, and 0 for a string, a capture, a bool or an unset variable. That covers every value=/variable= action, the arithmetic actions, <test>, <pause variable=> and [fill]. sipr parsed numeric strings and read a true bool as 1, so for one, <assignstr value="5"/><add value="1"/> gave 6 where sipp gives 1. Behavior change: a string or an <ereg> capture used as a number now needs a <todouble> first, as in SIPp. <todouble> takes a string only when all of it parses, and otherwise leaves its target alone with SIPp’s “Invalid double conversion” warning, where sipr wrote 0.
  • 3PCC twin commands show up in -trace_msg and -trace_shortmsg as in SIPp: TCP control message sent|received [<n>] bytes: frames, with SIPp’s handling of the closing ESC, and S/R short lines. The “Unexpected control message received” lines go to -trace_msg and the call debug, where SIPp writes them, not to -trace_err. A command without a Call-ID is refused with SIPp’s wording. -trace_shortmsg now finds a Call-ID on a message’s first line, where a twin command has it.
  • ontimeout= works on a <send> and a <recvCmd>, as in SIPp, which reads it on every message. A send whose UDP retransmissions run out sends the call to its ontimeout label with SIPp’s “timeout on max UDP retrans … jumping to label” warning, where the call used to fail. A recvCmd that -recv_timeout times out follows it the way a recv does. On <pause>, <nop> and <sendCmd> it compiles with a warning that it has no effect there, as SIPp ignores it. sipr warned it away as an unknown attribute. A send now times out one interval after its last retransmission, when SIPp’s timer fires, not right after it, and it counts in the send’s -trace_counts _Timeout column.
  • -trace_counts gives a nop, a <sendCmd> and a <recvCmd> the <index>_Pause_Sessions and <index>_Pause_Unexp columns, as real sipp does. SIPp’s count file tests for a pause in a way that is true for every message that is neither a send nor a recv. sipr wrote no columns for a nop, and SendCmd/RecvCmd columns SIPp never writes, so its header did not match sipp’s.
  • [msg_index] and [branch] render as SIPp does where it renders with no message index: in action messages (<log>, <assignstr>, …), <sendCmd> bodies and the -default_behaviors messages. There [msg_index] prints -1, and [branch] ends in the index of the message the call is at, minus one. sipr printed the current message’s index in both, so, for one, an abort BYE did not repeat the branch sipp gives it. [branch+N] and [branch-N] now add their offset, as SIPp’s docs and its pfca_* scenarios use them. sipr sent them through verbatim with an unknown-keyword warning.
  • A <jump> lands where SIPp’s does. SIPp’s jump to message N sets its message index to N-1 and lets the step carry on, and sipr now does the same. The actions after a jump still run, and the last jump wins. A nop or send then follows message N-1’s next= when it has one (its test set, its chance won) instead of running N. A send whose actions jump is sent first. A jump in a mandatory recv’s actions, or in a recvCmd’s, is overwritten by the step’s own next(). An optional recv that stays where the call waited leaves the call waiting at message N-1. A jump to the message the call is at fails the call with SIPp’s “jumps to itself” error, where sipr used to overflow its stack and crash. A recvCmd reached past an optional recv now moves the call on after its command. The unreachable lint follows the same rules.
  • <assign assign_to="x" value="7"/>, the form SIPp documents, compiles and stores the double 7. sipr took only variable= and refused the scenario (“<assign> needs a ‘variable’ attribute”). As in SIPp, variable= now stores the other variable’s number instead of a copy of its value, and giving both attributes or neither is an error.
  • -trace_screen writes <scenario>_<pid>_screen.log, the name real sipp uses, instead of _screens.log. That name came from sipp’s help text, which does not match the file sipp writes.
  • The scenario screen no longer panics on a step label longer than its 22-character column when the cut falls inside a multi-byte character (a non-ASCII display= text). Labels are cut by character, not byte.
  • A matched <recv> follows its next=, test= and chance= as in SIPp: it jumps to the next= label when the test= variable, if any, is set and the chance= draw, if any, is won, and otherwise moves on to the message after it. The recv’s actions run first, so an <ereg> on the recv can decide its own test=. An optional recv whose test= variable is unset leaves the call waiting where it was, and the receive timeout keeps its original deadline. sipr ignored these attributes on a recv, so it never took the jump. One divergence is deliberate: SIPp never takes an optional recv’s next= to a label before the first message (it reads index 0 as false); sipr takes it.
  • Receive timeouts follow SIPp: the recv the call waits at arms its own timeout= (else -recv_timeout), optional or not, and its ontimeout is the one taken. sipr armed the timeout of the window’s mandatory recv instead, so an optional recv’s timeout=/ontimeout did nothing and a scenario ending in <recv optional="true" timeout="1000" ontimeout="end"/> never ended. An ontimeout past the last message now fails the call (FailedTimeoutOnRecv) as in SIPp, where it counted as a success. -recv_timeout now also covers <recvCmd>, and -trace_err logs SIPp’s “receive timeout on message …” warnings. Behavior change: a timeout= on a mandatory recv behind optional ones runs only once an optional match has moved the call onto it, as in SIPp.
  • <label>s no longer count as messages. SIPp numbers only message commands, and a label names the index of the message after it. sipr counted labels, so with a label earlier in the scenario, <jump value="N"/> and <jump variable=> landed on the wrong step, and [msg_index], [branch], _unexp.retaddr and the -trace_counts column prefixes were off. A jump to the message count (past the last message) is now accepted and ends the call, as in SIPp. The --check dump, /scenario, /metrics step=, dump tasks and the call-debug trace use the same message numbering.
  • Without -i, [local_ip] rendered 0.0.0.0. It now renders the address the remote is reached from, found as SIPp does by connecting a UDP socket and reading it back (no packet is sent). Sockets still bind every interface unless -i or -bind_local names one (M44).
  • On Windows an ICMP port-unreachable for a peer that is down no longer kills the UDP socket: the receive loop rides out ConnectionReset (the WSAECONNRESET quirk on unconnected sockets) instead of ending the run with “socket error”.

0.27.1 — 2026-09-20

Changed

  • The release workflow publishes crates one at a time, skipping versions already on crates.io, so a partial publish resumes instead of failing on the first already-published crate. A manually triggered “Publish crates” workflow does the same for any existing tag.

0.27.0 — 2026-09-20

Security

  • rustls updated to 0.23.45 for RUSTSEC-2026-0285 (TLS 1.3 handshake messages accepted across encryption level boundaries).

Added

  • Release automation modelled on gitwig’s: a tag-triggered CD workflow that builds static Linux (x86_64, arm64), macOS (Intel, Apple Silicon) and Windows binaries, publishes the GitHub release, and, when the matching secret is present, publishes the crates to crates.io, updates the tareqmy/homebrew-tap formula, and pushes a Chocolatey package.
  • Install and uninstall scripts for macOS/Linux (scripts/install.sh) and Windows (scripts/install.ps1), a Nix flake, a reference Homebrew formula, and docs/INSTALLATION.md covering every method.
  • cargo-deny policy (deny.toml) checked in CI, and a CI portability job building and testing on macOS and Windows.
  • SECURITY.md and CONTRIBUTING.md.

Changed

  • TLS PEM files (-tls_cert, -tls_key, -tls_ca, -tls_crl) are now parsed with rustls-pki-types, the crate rustls itself uses, replacing the unmaintained rustls-pemfile (RUSTSEC-2025-0134). Same formats, and a malformed file is now reported with its name.
  • README rewritten to describe sipr’s relationship to SIPp neutrally, state the benchmark numbers benches/BASELINES.md actually records, and list every deliberate compatibility gap.

Fixed

  • The call pacer now credits new calls from elapsed wall-clock time (rate × elapsed / rate_period, as SIPp does) instead of counting timer ticks, so a delayed or coalesced tick no longer lowers the achieved rate.

0.26.0 — 2026-09-19

Added

  • exec command= (M37): run a shell command from an action, with keywords and variables rendered into it. Fire-and-forget as in SIPp, but spawned and reaped by one runner thread, so the engine never forks or blocks and no zombies accumulate under load.
  • <setdest host= port= protocol=/> (M37): send the rest of a call to another peer — retargeting over UDP, re-dialling the call’s own connection over per-call TCP — with every check SIPp makes and its wording. A rejected setdest fails that call rather than the run. [remote_ip]/[remote_port] keep the nominal remote, as in SIPp.
  • ereg search_in="body" and search_in="var" variable="…", which SIPp’s documented setdest idiom relies on.

Notes

  • Three SIPp behaviors met on the way are recorded in docs/SIPP_COMPAT.md §6 and deliberately not copied: [next_url] needs rrs="true" to carry the Contact in SIPp, [last_*] inside a recv’s own actions still name the previous message there, and SIPp’s echo [last_From] example breaks under any shell without quoting.

0.25.0 — 2026-09-19

Added

  • Manual transactions (M36): start_txn and ack_txn on <send>, response_txn on <recv>. A recv so named matches only a response whose top Via branch is the one its request carried, which tells concurrent transactions of the same method apart. SIPp’s placement and usage errors are reported with its wording, requests naming a transaction leave the CSeq-method guard list, and late responses to a named transaction are handled as SIPp does: a provisional is ignored, a final one for an INVITE transaction gets the recorded ACK sent again, and a repeat of the final already taken is ignored. --check lists the transactions. Verified against real sipp both ways.

0.24.0 — 2026-09-19

Added

  • Dynamic users (M35): <User variables="…"/> and <Global variables="…"/> give variables a life beyond the call — one table per user id, kept for the run, and one for the whole process, shared by both scenarios — resolved at compile time into a layered store with no allocation on the hot path. set users now follows SIPp’s id bookkeeping exactly: the pool is served from the back, a call ending while more calls are live than the target retires its id, and a later growth reactivates retired ids (with their variables) before creating fresh ones. SIPp’s -set VARIABLE VALUE seeds a global; dump variables over the control socket lists the scopes; --check prints them. Verified against real sipp both ways.

Fixed

  • Variable values now render and test exactly as in SIPp: a double is written with %lf (3.000000, sipr used to print 3), a true bool as true, and a zero double, a false bool or an unset variable as nothing at all; test="var" and condexec on a message take the same “is set” view (a "0" or "false" string is set, a zero counter is not). Scenarios that compared a rendered counter against 3 should compare against 3.000000 — that is what SIPp sends.

0.23.0 — 2026-09-13

Added

  • Mixed mode (-rxsf <file> / -rxsn <name>, -rxinf <file>) — a second, server-mode scenario terminates the calls the peer originates towards us while the client-mode main scenario originates ours, as in SIPp: own statistics, rx calls never counting toward -m/-l/-users, -rxinf files joining the injection table after the -inf ones, and set display rx over the control socket. sipr enforces the role rules SIPp’s help text only promises. Verified against real sipp both ways.

Changed

  • set display ooc|main|rx now switches every screen — counters, statistics, repartitions and the scenario page — to the displayed scenario, as SIPp does; 0.22.0 swapped only the scenario page. The HTTP /stats document gains a mixed flag.

0.22.0 — 2026-09-12

Added

  • Out-of-call scenarios (-oocsf <file> / -oocsn <name>) — a second, independently compiled scenario that answers requests mapping to no known call, as in SIPp: client mode only, the embedded ooc_default and ooc_dummy (dumpable with -sd), own per-step statistics, no [fieldN]/-inf in ooc calls, and ooc calls never count toward -m/-l/-users. set display ooc|main over the control socket swaps the TUI scenario page. Verified both ways against real sipp.

Fixed

  • regexp_match="true" on <recv> was parsed but never applied by the matcher, so a request=".*" step matched nothing.

0.21.0 — 2026-09-06

Added

  • SCTP transport (-t s1|sn) behind the new off-by-default sctp cargo feature, via socket2: one SCTP message per SIP message as SIPp does, mono and per-call associations, reconnection. Needs an OS SCTP stack at run time (Linux with the sctp module); SIPp’s SCTP option flags are rejected with an explanation. Exercised in Linux CI against a SIPp built with USE_SCTP.

Fixed

  • CI now runs on pushes to master (it only ran for pull requests) and builds SIPp 3.7.7 from source for the interop suite; a few tests that raced the echo threads’ counters or assumed macOS socket timing are settled.

0.20.0 — 2026-09-05

Added

  • -t ui — one UDP socket per IP address from the injection file (-ip_field): each client call sends from its line’s IP, a server binds every listed IP and answers on the one the request hit, and the new [server_ip] keyword renders the IP a call sends from — SIPp’s per-IP mode for emulating many user agents.

0.19.0 — 2026-09-05

Added

  • TCP/TLS reconnection — -max_reconnect, -reconnect_close, -reconnect_sleep: when the mono TCP/TLS connection drops, the calls on it fail (or, with -reconnect_close false, live on), the call whose send finds it dead fails, and the connection is re-dialed within the budget after the sleep — SIPp’s reset, in SIPp’s order; with no budget left the run ends with exit 255 like SIPp’s fatal error. Verified both ways against real sipp.

0.18.0 — 2026-09-05

Added

  • -rsa host[:port] — the remote sending address: a UAC sends every message there instead of to the target, a UAS answers there (from a socket of its own) instead of to the request’s source, and the keywords keep naming the nominal remote, as in SIPp. Verified both ways against real sipp.

0.17.0 — 2026-09-05

Added

  • Per-call sockets — -t un, -t tn, -t ln: every call opens its own UDP socket or TCP/TLS connection at its first send (SIPp’s multisocket modes), [local_port] names it, and -max_socket caps how many are open before calls share them round-robin. <closecon/> now closes a per-call socket for real. Verified both ways against real sipp.

0.16.0 — 2026-09-05

Added

  • SIPp’s unexpected-message handler — <label id="_unexp.main"/>, _unexp.retaddr, _unexp.pausedaddr, <jump variable=> and <pauserestore>: an unexpected in-call message jumps to the handler, which answers it and resumes the interrupted pause for exactly its remaining time. Verified both ways against real sipp.
  • <closecon/> is accepted (a no-op, as SIPp’s reference-count drop is on every mono-socket transport).

0.15.0 — 2026-09-05

Added

  • <verifyauth> — sipr can play a digest-checking registrar: the received Authorization: header is verified against a username and password (MD5 or SHA-256, qop auth/auth-int, -auth_uri) and the boolean verdict drives test= branching, exactly SIPp’s documented recipe. Verified in both directions against real sipp.

0.14.0 — 2026-09-05

Added

  • SRTP echo server — exec rtp_echo="startaudio|updateaudio|stopaudio| startvideo|updatevideo|stopvideo[,pt[,name]]": the call echoes (S)RTP on its advertised media port, re-keyed from the SDES negotiation with the caller’s SSRC and sequence numbers preserved. SIPp’s pfca_uas_*_crypto_*.xml scenarios now run unchanged, and real sipp’s UAC passes its own RTP check against them.

Fixed

  • ereg search_in="hdr" now hands the regexp what SIPp does: the rest of the first matching line after the header string (so header="CSeq:" works and CSeq: [$1] replays the caller’s CSeq), and an absent header fails the call under check_it.

0.13.0 — 2026-09-05

Added

  • [authentication] from an injection field — a CSV column holding [authentication username=… password=…] (or AKA parameters) is re-parsed as the keyword at send time, SIPp’s documented way to give each call its own credentials.

Fixed

  • [authentication] now renders the whole header line as SIPp does (Authorization: after a 401, Proxy-Authorization: after a 407), so SIPp scenarios that place the keyword on its own line work unchanged. sipr’s earlier Authorization: [authentication …] spelling still works.

0.12.0 — 2026-09-05

Added

  • SRTP with SDES keying — SIPp’s crypto keywords ([cryptotag1audio], [cryptosuiteaescm128sha1801audio], [cryptokeyparams1audio], the ue… unencrypted forms, secondary and video variants) render offers and answers; the peer’s a=crypto: lines are parsed; rtp_stream packets are protected with AES-CM-128 or the NULL cipher and HMAC-SHA1 80/32, and the echo check unprotects the echo before comparing. All cryptography is in-tree and verified against RFC 3711’s vectors. Unlike SIPp, the authentication tag uses the packet’s own rollover counter, so streams stay valid past sequence 65535.

Fixed

  • The CSeq-method guard on recv response= now follows SIPp exactly: a response matches when its CSeq method is any request method sent so far, not only the most recent one. A 200 to the INVITE arriving after a PRACK was wrongly treated as unexpected.

0.11.0 — 2026-09-04

Added

  • hide and display attributes, and SIPp’s screen keys — hide="true" keeps a step off the scenario screen while set hide true (the default) holds; display="…" replaces its label. Both reach /stats. The 1/2/3 keys switch screens at the keyboard and over the control socket.

0.10.0 — 2026-09-04

Added

  • -auth_uri — SIPp’s flag for the digest uri=; the value gets a sip: prefix exactly as SIPp does.
  • Keywords inside [authentication] parameters — username=[field0], password=[$p], aka_K=[field2] and friends are rendered before use, as SIPp renders them, so credentials can come from injection files.

Changed

  • The default digest uri= is now SIPp’s sip:remote_ip:remote_port (no user part) instead of sip:service@remote_ip:remote_port. Servers verify against the header’s own uri=, so runs are unaffected; the wire form now matches SIPp byte for byte.

0.9.0 — 2026-09-04

Added

  • Rate ramps — SIPp’s -rate_increase N, -rate_interval TIME (seconds or ms/s/m/h), -rate_max N, and -no_rate_quit: the rate climbs every interval and, when it would pass the cap, is clamped there and the run drains (unless told not to). Also -rate_scale for the hot-key step.

0.8.0 — 2026-09-04

Added

  • AKA resynchronisation (AUTS) — [authentication … aka_sqn=0x…] gives the client’s SQN_MS; a challenge whose SQN is not above it (or any challenge with aka_resync=1) is answered with auts= and an empty-password digest per RFC 3310 §3.2 / TS 33.102 §6.3.3, then the server’s fresh challenge is answered normally. SIPp’s resync code is unreachable, so this is new ground for SIPp scenarios.

0.7.0 — 2026-09-04

Added

  • RTP echo and the RTP check — -rtp_echo (with -mb) echoes RTP received on the media port and media port + 2 back to its sender, with SIPp’s counters and the <rtp_echo value="0|1"/> action to toggle it; rtp_stream sockets now read back what the peer echoes and compare it to what was sent, and -audiotolerance / -videotolerance turn that into SIPp’s verdict: a failed check exits 253 (SIPp’s -3). Unlike SIPp, a stream is judged only when a tolerance flag is given. New counters on the TUI, the -bg line, and /stats.

0.6.0 — 2026-09-04

Added

  • Runtime control — SIPp’s UDP control socket (-cp, -ci: hot keys and c-prefixed set/trace/dump/reset commands with SIPp’s grammar and warning texts; default bind is loopback and -cp 0 disables it) and a new HTTP/JSON API (--sipr-http [HOST:]PORT, --sipr-http-token): /health, /stats, /control, /quit, /command, /scenario. See docs/CONTROL_API.md. New std-only crate sipr-control.
  • Hot keys now follow SIPp exactly: set rate-scale steps, user-count keys in -users mode, and a second q aborts like Q.

0.5.0 — 2026-09-04

Added

  • IMS AKA authentication (AKAv1-MD5, RFC 3310) — [authentication aka_K=0x… aka_OP=0x… aka_AMF=0x…] (SIPp’s parameters, plus aka_OPc=) against an algorithm=AKAv1-MD5 challenge: the nonce’s RAND/AUTN go through an in-tree Milenage (AES-128, verified on 3GPP TS 35.208 test sets), the MAC is checked, and RES becomes the digest password. A MAC mismatch fails the call with a clear reason where SIPp aborts the whole process. No new dependency.

0.4.0 — 2026-09-04

Added

  • RTP streaming and DTMF (exec rtp_stream=, exec play_dtmf=) — SIPp’s rtpstream.cpp semantics on the M14 scheduler: raw codec files or apattern/vpattern fills with SIPp’s fixed payload table, looping, pause/resume (the clock keeps running, as in SIPp), SSRC 0xCA110000-based, plus RFC 4733 DTMF bursts with SIPp’s exact timing. [rtpstream_audio_port]/[rtpstream_video_port] keywords with per-call allocation, -rtp_payload, -max_rtp_port, -random_base_ssrc. sipr streams from the port the SDP advertised (SIPp binds an unrelated one) and numbers DTMF packets consecutively (SIPp skips every other warm-up number). Divergences in docs/SIPP_COMPAT.md §6.

0.3.0 — 2026-09-04

Added

  • pcap replay (exec play_pcap_audio|video|image=) — SIPp’s media feature, without the raw socket: a new std-only sipr-media crate reads classic pcap files (Ethernet/802.1Q, raw IP, Linux cooked, BSD loopback; IPv4/IPv6 UDP), learns the peer’s media endpoint from its SDP, and replays the UDP payloads verbatim on the capture’s timeline from one scheduler thread, through ordinary UDP sockets bound to the advertised media port — no root, no libpcap. -mi/-mp (-min_rtp_port), [auto_media_port], [media_port+N], <recv ignoresdp>. RTP counters on the TUI, -bg line, and the final summary. Divergences from SIPp in docs/SIPP_COMPAT.md §6.

0.2.0 — 2026-09-03

Added

  • TLS transport (-t l1) — SIP over TLS with SIPp’s exact semantics: same connection-per-peer model and Content-Length framing as TCP, no SIP retransmissions, port 5060, [transport] renders TLS. -tls_cert / -tls_key (defaults cacert.pem/cakey.pem), -tls_ca / -tls_crl (presence enables SIPp-style verification: chain but not hostname on the client, mandatory client cert on the server), -tls_version 1.2|1.3. Built on rustls with the ring provider — the workspace’s first external dependency, still no system OpenSSL required. Divergences from SIPp documented in docs/SIPP_COMPAT.md §6.

Changed

  • The dependencies: std-only claim is retired: sipr-net now carries rustls/rustls-pemfile for the TLS transport. Everything else remains std; the build still needs no system libraries.

  • IPv6 — targets accept bracketed ([::1], [2001:db8::1]:5060) and bare-literal (::1) IPv6, with automatic :: binding when a v6 target is given without -i. [local_ip]/[remote_ip] render bracketed inside URIs and Via (SIPp’s local_ip_w_brackets), while [media_ip] stays raw for SDP. See docs/SIPP_COMPAT.md §6.

0.1.1 — 2026-08-17

Post-v1 feature drop: TCP transport, injection files with indexed lookups, classic 3PCC, and closed-loop -users mode. Still standard-library only.

Added

  • Closed-loop -users — -users N keeps N concurrent calls, each holding a stable 1-based user id; a finished call’s id is recycled into a replacement immediately. Adds the [userid]/[users] keywords and lights up USER-mode -inf injection (line = user id − 1). Mutually exclusive with -l. See docs/SIPP_COMPAT.md §6.
  • Classic 3PCC — -3pcc HOST:PORT plus <sendCmd>/<recvCmd> steps let two sipr instances coordinate over a separate ESC-delimited TCP “twin” socket. The role is derived from the scenario’s first twin command (sendCmd-first dials, recvCmd-first listens); <recvCmd> blocks the call until a command arrives and runs its actions against the command text. See docs/SIPP_COMPAT.md §6.
  • TCP transport — -t t1 runs SIP over TCP. A stream framer de-frames messages by Content-Length (RFC 3261 §7.5); the client keeps one connection to the target, the server accepts connections and replies on the one each request arrived on. Reliable transport, so no SIP retransmissions are scheduled. -t tn is accepted as an alias. See docs/SIPP_COMPAT.md §6.
  • Injection files — -inf FILE (repeatable) loads SIPp-style injection files: a SEQUENTIAL/RANDOM/USER mode header, ;-separated fields, # comments, blank-line terminator. One line is drawn per call per file (SEQUENTIAL cycles, RANDOM picks uniformly, USER defers to -users). The [fieldN] keyword substitutes field N of the drawn line. file=NAME selects another file by its basename (SIPp’s key), or by a 0-based -inf index (sipr extension); line= overrides the per-call line and is rendered at send time, so line=[$var] works. Unknown field/file names are rejected at load. See docs/SIPP_COMPAT.md §6.
  • Indexed injection — -infindex FILE FIELD builds a key→line index over one field of an -inf file (matched by basename; last line wins on duplicate keys). Actions <lookup assign_to=… file=… key=…/> (stores the matched line or -1), <insert file=… value=…/> (appends a row), and <replace file=… line=… value=…/> (swaps a row) operate on that data at runtime. The canonical use is lookup → [fieldN line=[$var]].

Fixed

  • Body-less SIP messages (180, ACK, empty 200) now always include the mandatory \r\n\r\n header/body separator. UDP tolerated its absence; TCP framing and real-SIPp interop require it.

0.1.0 — 2026-08-16

First release. A SIPp-compatible SIP testing tool and traffic generator, feature-complete for signaling over UDP. Built entirely on the Rust standard library — no external crates.

Added

  • Scenarios — SIPp-compatible XML: send, recv, pause, nop, label, timewait, Reference, and the response/call-length repartition tables. Loud diagnostics with file:line context; --check lint mode that prints the compiled IR.
  • Keywords — [service], [remote_ip]/[remote_port], [local_ip]/[local_port], [transport], [call_id], [call_number], [cseq], [branch], [msg_index], [pid], [routes], [next_url], [peer_tag_param], [len], [last_*:], [$var], [authentication], and the [media_*] placeholders.
  • Actions — ereg (capture groups via an in-tree POSIX-ERE engine), assign/assignstr/strcmp/test, arithmetic (add/subtract/ multiply/divide), todouble, trim, urlencode/urldecode, gettimeofday, jump, log/warning/error, exec int_cmd. Plus test/condexec branching, chance, named counters, and per-call variables.
  • Engine — UAC and UAS roles; open-loop pacer (-r/-rp/-l/-m) with rate smoothing; single event-loop over UDP, timers, and the pacer. Recv matching (optional-recv windows, backward contiguous scan, CSeq-method guard) verified against SIPp’s call.cpp.
  • Transport — UDP with RFC 3261 T1→T2 retransmission, inbound retransmission handling, simulated loss (lost), and an in-tree SIP message parser proven panic-free by a fuzz suite.
  • Authentication — digest MD5 and SHA-256 (qop=auth, proxy 407), with in-tree hash primitives verified against the RFC 1321 / FIPS 180-4 / RFC 2617 / RFC 7616 vectors.
  • Statistics — SIPp counter set with failure breakdown, response-time histograms and RTDs, repartition tables, -trace_stat/-stf/-fd CSV, and -trace_msg/-trace_err files.
  • Live TUI — main / per-step scenario / repartition screens, live rate keys (+ - * /), pause (p), screen cycling (s), and safe terminal restore on every exit path. Ferrous brand colors, honoring NO_COLOR.
  • CLI — SIPp-style single-dash flags with did-you-mean suggestions; SIPp-compatible exit codes (0 ok, 1 failures, 99 no calls, 2 usage, 255 fatal).
  • Tooling — six-crate workspace, Makefile convenience targets, CI (fmt + clippy + tests, and an interop job against real SIPp), and the Ferrous brand kit under brand/.

Known limitations

  • Signaling only over UDP. TCP/TLS, -inf injection files (and the lookup/insert/replace actions), 3PCC, RTP/pcap media, IPv6, and an HTTP control API are on the post-v1 roadmap.
  • The ereg regex engine is leftmost-first greedy (PCRE-style), not POSIX leftmost-longest — identical on the patterns real scenarios use; see docs/SIPP_COMPAT.md §6.