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 5000no 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 aNetSink. ASender<NetEvent>converts into one, so existing callers compile unchanged;NetSink::wrappingdelivers into a channel of the caller’s own events.TimerService::cancelno longer wakes the timer thread.
0.32.0 — 2026-09-30
Added
--sipr-ws-pathand--sipr-ws-origin(M53): the resource on the WebSocket upgrade’s request line (default/) and anOriginheader, 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_requestholds aWsRequest, andserver_handshakenow returns aServerUpgradenaming the path andOrigina client asked for.
0.31.0 — 2026-09-28
Changed
- The RTP echo check is judged at SIPp’s defaults (M52): every
rtp_streamthat 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_tolerancearef64now, defaulting toDEFAULT_RTP_TOLERANCE.
Added
-rtpcheck_debug(M52) writes the RTP check’s trace to SIPp’sdebugafile(audio) anddebugvfile(video) in the working directory: one line per send (hex, header included), per datagram read back, and per verdict with the runningfailed/senttally, plus the stream’s tally when it ends. The flag was accepted and ignored before;-srtpcheck_debugstill is.
0.30.0 — 2026-09-28
Added
- SIP over WebSocket (M51, RFC 7118; a sipr addition, SIPp has none):
-t ws1|wsnon TCP and-t wss1|wssnon TLS, one connection per peer or per call liket1|tn|l1|ln, with the same reconnection,-rsaand-max_socketbehavior;[transport]rendersWS/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 itsrtd=,crlf,hide,displayandcondexecapply — and a pause’s duration (variable=, a distribution, or-dwhen bare).next=andontimeout=on it are compile errors, as SIPp refuses them. sipr used to accept onlymillisecondsthere 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=andrtd=are booked on entry, where SIPp’sdo_bookkeepingruns 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_rttrows carry milliseconds, as their column names say and as real sipp writes them: its clock counts microseconds since the process started andcomputeRttdivides 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.verifyauthno longer panics on an Authorization header whosealgorithm=value carries a non-UTF-8 byte in its first three: the MD5/SHA-256 prefix check compared astrslice at a fixed byte index, which split the replacement character the lossy decode had put there. Found by the newauthfuzz target on its fourth input.- A
PRINTF=injection field whose%dwidth 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 theinjection_csvfuzz target.
0.29.0 — 2026-09-27
Added
- The library API is documented and promised (M49 D7/D8):
docs/LIBRARY_API.mdis the reference forsipr-enginein a Rust harness, the crate documentation lists the supported surface, andcrates/sipr-engine/examples/embed.rsruns a UAS and a UAC in one process (cargo run -p sipr-engine --example embed). Within a0.xminor 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--checkpolicy as a function — the lints run, and any diagnostic, a warning included, is anErrcarrying every diagnostic.sipr_engine::Run, the library entry point (M49 D2,docs/LIBRARY_API.md):Run::start(scenario, config)(orstart_withfor 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 anEngineControlwith the newpause,resume,stop,abortandkeynext toset_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; andwait()joins the thread and returns theRunReport. Dropping aRunwithout 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 feedingEngineControl::key.EngineError::Fatal: the engine thread could not be started, or ended without a report.
Fixed
- Call numbers —
[call_number], the%uof 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,renderand friends) and the blockingrun_with_*entry points are hidden fromsipr-engine’s documentation: reachable for the bench and earlier callers, not part of the supported surface, free to change. EngineConfig::infandrxinf(formerlyinf_filesandrx_inf_files) holdInjectionSources: aPathread at start-up, orText { 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 NAMErefer to, as before (M49 D5).run_with_control,run_with_uiandrun_scenariosare hidden from the documentation and kept for one minor release;UiChannels::snapshotsis a boundedSyncSender, and a reader that falls behind misses snapshots instead of growing a queue or stalling the engine.sipr_engine::EngineErroris an enum —Config,Scenario,Bind(with the OS error as itssource()) andIo(with the path and the OS error) — that implementsstd::error::Error, so?intoanyhoworBox<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:anderror:line, the-bgstatistics line — is asipr_engine::Noticesent whereEngineConfig::noticespoints: stderr with the same wording by default, a channel for an embedder, or nowhere (NoticeSink::Discard, an explicit choice).sipr-enginedenies clippy’sprint_stderr/print_stdoutso it stays that way (M49 D3).-nostdinis 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.UiChannelstakes each half as anOption, so a key producer needs no snapshot reader. sipr_engine::EngineConfighasDefault(SIPp’s defaults) and the constructorsEngineConfig::uac(target)andEngineConfig::uas(), and is#[non_exhaustive], as isRunReport: 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.TlsConfigandLogOverwritegainedPartialEq.
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 aCounter <name>row per counter with its periodic and cumulative values.-trace_statwrites a<name>(P)and<name>(C)column pair per counter afterCallLengthStDev, namedGenericCounter<name>for an all-digit name and cut to SIPp’s 19 characters; the header matches real sipp’s byte for byte./statsand--sipr-stats-jsoncarry acounterslist, and/metricsasipr_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) andcontent-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 aswarning[NAME], and<!-- sipr-lint: allow NAME -->right before a step silences one for that step. Seedocs/LINTS.md. - Structured statistics (M46):
--sipr-stats-json FILEappends one JSON object per snapshot tick (the/statsdocument), 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 /metricson the HTTP API serves the same snapshot as Prometheus text exposition: names prefixedsipr_, 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. Seedocs/CONTROL_API.md§3. - Performance measurement (M45):
make bench-vs-sippruns 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 inbenches/BASELINES.md, and a nightly Linux workflow runs both. The results, including where sipp is still faster, are indocs/PERFORMANCE.md. PRINTF=injection files (M44): aPRINTF=<n>header, with optionalPRINTFOFFSET=andPRINTFMULTIPLE=, turns the data rows into templates over n virtual lines, so one row can stand in for thousands of users.-infindexand-usersrange over the virtual lines;insert/replaceare 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-iaddress 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_deviceon Linux (SO_BINDTODEVICE, needs root; a usage error on other systems). play_pcap_audio/_video/_imageread 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 NAMEwith-slave_cfg FILE(name;host:portlines),sendCmd dest=routed to the named peer andrecvCmd src=checked against the command’sFrom:line, on SIPp’s wiring (every instance listens on its table address; the master dials itsdest=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 optionalrecvCmdlets 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_callis 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 frommasterby 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_retransas 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-ndasnone) 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_timeoutand-timer_resolare accepted with a warning. The[last_Request_URI]keyword. - Message and error logs at parity (M41):
-trace_msgframes and-trace_errlines take SIPp’s exact shapes (timestamps, theThe 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>_overwriteflags,-ringbuffer_files/-ringbuffer_size/-max_log_sizerotation and-deadcall_waitare implemented as SIPp’s;-trace_timeoutis accepted (a no-op in SIPp too).trace logs|shortmessages on|offwork on the control socket. - Statistics files at parity (M40):
-trace_statwrites 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_rtdand-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 VALUEdefines generic keywords. New flags-tdmmap,-dynamicStart/-dynamicMax/-dynamicStepand-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’ssanity_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
--checkfails 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,-pluginand 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_warnmakes 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-ndor-default_behaviors …,-bye; a///prefix in an inbound Call-ID is stripped as SIPp’s 3PCC marker unless-callid_slash_ign. - The
-trace_msgframe no longer carries the peer address, and-trace_errlines 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. -fddefaults to 60 s as in SIPp (it was 1 s); the final statistics row is still written at exit. The-trace_statheader changed from sipr’s earlier subset to SIPp’s columns, so parsers keyed on column position need SIPp’s layout.--checkand 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 /quitarrives in full. A soft quit with little to drain could end sipr between the reply’s head and body, so a client read a202with 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_Retranscolumn, and the resend on the send’s. A recv’slost=(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 (“unexpectedinside ”). - With packet loss on (
-lost, or anylost=), each simulated loss counts on its step, and-trace_countsgives every send and recv SIPp’s_Lostcolumn. The scenario screen shows alostcolumn. sipr dropped the messages but counted nothing, so its counts header was shorter than sipp’s. - The
+N/-Nkeyword 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 gets0. - 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 aPause_Sessionsentry in-trace_counts, as a pause does. - A false bool, such as a failed
<test>result, renders asfalsein[$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
getDoublereads it: a double’s value, and 0 for a string, a capture, a bool or an unset variable. That covers everyvalue=/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_msgand-trace_shortmsgas in SIPp:TCP control message sent|received [<n>] bytes:frames, with SIPp’s handling of the closing ESC, andS/Rshort lines. The “Unexpected control message received” lines go to-trace_msgand the call debug, where SIPp writes them, not to-trace_err. A command without a Call-ID is refused with SIPp’s wording.-trace_shortmsgnow 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 itsontimeoutlabel with SIPp’s “timeout on max UDP retrans … jumping to label” warning, where the call used to fail. A recvCmd that-recv_timeouttimes 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_Timeoutcolumn.-trace_countsgives a nop, a<sendCmd>and a<recvCmd>the<index>_Pause_Sessionsand<index>_Pause_Unexpcolumns, 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, andSendCmd/RecvCmdcolumns 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_behaviorsmessages. 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 itspfca_*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’snext=when it has one (itstestset, itschancewon) 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 ownnext(). 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. Theunreachablelint follows the same rules. <assign assign_to="x" value="7"/>, the form SIPp documents, compiles and stores the double 7. sipr took onlyvariable=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_screenwrites<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 itsnext=,test=andchance=as in SIPp: it jumps to thenext=label when thetest=variable, if any, is set and thechance=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 owntest=. An optional recv whosetest=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’snext=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 itsontimeoutis the one taken. sipr armed the timeout of the window’s mandatory recv instead, so an optional recv’stimeout=/ontimeoutdid nothing and a scenario ending in<recv optional="true" timeout="1000" ontimeout="end"/>never ended. Anontimeoutpast the last message now fails the call (FailedTimeoutOnRecv) as in SIPp, where it counted as a success.-recv_timeoutnow also covers<recvCmd>, and-trace_errlogs SIPp’s “receive timeout on message …” warnings. Behavior change: atimeout=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.retaddrand the-trace_countscolumn prefixes were off. A jump to the message count (past the last message) is now accepted and ends the call, as in SIPp. The--checkdump,/scenario,/metricsstep=,dump tasksand the call-debug trace use the same message numbering.- Without
-i,[local_ip]rendered0.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-ior-bind_localnames 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(theWSAECONNRESETquirk 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
CDworkflow 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 thetareqmy/homebrew-tapformula, 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, anddocs/INSTALLATION.mdcovering every method. cargo-denypolicy (deny.toml) checked in CI, and a CI portability job building and testing on macOS and Windows.SECURITY.mdandCONTRIBUTING.md.
Changed
- TLS PEM files (
-tls_cert,-tls_key,-tls_ca,-tls_crl) are now parsed withrustls-pki-types, the crate rustls itself uses, replacing the unmaintainedrustls-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.mdactually 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 rejectedsetdestfails that call rather than the run.[remote_ip]/[remote_port]keep the nominal remote, as in SIPp.ereg search_in="body"andsearch_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]needsrrs="true"to carry the Contact in SIPp,[last_*]inside a recv’s own actions still name the previous message there, and SIPp’secho [last_From]example breaks under any shell without quoting.
0.25.0 — 2026-09-19
Added
- Manual transactions (M36):
start_txnandack_txnon<send>,response_txnon<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.--checklists 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 usersnow 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 VALUEseeds a global;dump variablesover the control socket lists the scopes;--checkprints 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 print3), a true bool astrue, and a zero double, a false bool or an unset variable as nothing at all;test="var"andcondexecon 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 against3should compare against3.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,-rxinffiles joining the injection table after the-infones, andset display rxover 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|rxnow 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/statsdocument gains amixedflag.
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 embeddedooc_defaultandooc_dummy(dumpable with-sd), own per-step statistics, no[fieldN]/-infin ooc calls, and ooc calls never count toward-m/-l/-users.set display ooc|mainover 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 arequest=".*"step matched nothing.
0.21.0 — 2026-09-06
Added
- SCTP transport (
-t s1|sn) behind the new off-by-defaultsctpcargo feature, viasocket2: 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 thesctpmodule); SIPp’s SCTP option flags are rejected with an explanation. Exercised in Linux CI against a SIPp built withUSE_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_socketcaps 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 receivedAuthorization:header is verified against a username and password (MD5 or SHA-256, qop auth/auth-int,-auth_uri) and the boolean verdict drivestest=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’spfca_uas_*_crypto_*.xmlscenarios 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 (soheader="CSeq:"works andCSeq: [$1]replays the caller’s CSeq), and an absent header fails the call undercheck_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 earlierAuthorization: [authentication …]spelling still works.
0.12.0 — 2026-09-05
Added
- SRTP with SDES keying — SIPp’s crypto keywords (
[cryptotag1audio],[cryptosuiteaescm128sha1801audio],[cryptokeyparams1audio], theue…unencrypted forms, secondary and video variants) render offers and answers; the peer’sa=crypto:lines are parsed;rtp_streampackets 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
hideanddisplayattributes, and SIPp’s screen keys —hide="true"keeps a step off the scenario screen whileset hide true(the default) holds;display="…"replaces its label. Both reach/stats. The1/2/3keys switch screens at the keyboard and over the control socket.
0.10.0 — 2026-09-04
Added
-auth_uri— SIPp’s flag for the digesturi=; the value gets asip: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’ssip:remote_ip:remote_port(no user part) instead ofsip:service@remote_ip:remote_port. Servers verify against the header’s ownuri=, 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 orms/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_scalefor 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 withaka_resync=1) is answered withauts=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_streamsockets now read back what the peer echoes and compare it to what was sent, and-audiotolerance/-videotoleranceturn 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-bgline, and/stats.
0.6.0 — 2026-09-04
Added
- Runtime control — SIPp’s UDP control socket (
-cp,-ci: hot keys andc-prefixedset/trace/dump/resetcommands with SIPp’s grammar and warning texts; default bind is loopback and-cp 0disables it) and a new HTTP/JSON API (--sipr-http [HOST:]PORT,--sipr-http-token):/health,/stats,/control,/quit,/command,/scenario. Seedocs/CONTROL_API.md. New std-only cratesipr-control. - Hot keys now follow SIPp exactly:
set rate-scalesteps, user-count keys in-usersmode, and a secondqaborts likeQ.
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, plusaka_OPc=) against analgorithm=AKAv1-MD5challenge: 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’srtpstream.cppsemantics on the M14 scheduler: raw codec files orapattern/vpatternfills with SIPp’s fixed payload table, looping,pause/resume(the clock keeps running, as in SIPp), SSRC0xCA110000-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 indocs/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-onlysipr-mediacrate 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,-bgline, and the final summary. Divergences from SIPp indocs/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]rendersTLS.-tls_cert/-tls_key(defaultscacert.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 onrustlswith theringprovider — the workspace’s first external dependency, still no system OpenSSL required. Divergences from SIPp documented indocs/SIPP_COMPAT.md§6.
Changed
-
The
dependencies: std-onlyclaim is retired:sipr-netnow carriesrustls/rustls-pemfilefor 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’slocal_ip_w_brackets), while[media_ip]stays raw for SDP. Seedocs/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 Nkeeps 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-infinjection (line = user id − 1). Mutually exclusive with-l. Seedocs/SIPP_COMPAT.md§6. - Classic 3PCC —
-3pcc HOST:PORTplus<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. Seedocs/SIPP_COMPAT.md§6. - TCP transport —
-t t1runs SIP over TCP. A stream framer de-frames messages byContent-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 tnis accepted as an alias. Seedocs/SIPP_COMPAT.md§6. - Injection files —
-inf FILE(repeatable) loads SIPp-style injection files: aSEQUENTIAL/RANDOM/USERmode 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=NAMEselects another file by its basename (SIPp’s key), or by a 0-based-infindex (sipr extension);line=overrides the per-call line and is rendered at send time, soline=[$var]works. Unknown field/file names are rejected at load. Seedocs/SIPP_COMPAT.md§6. - Indexed injection —
-infindex FILE FIELDbuilds a key→line index over one field of an-inffile (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 islookup → [fieldN line=[$var]].
Fixed
- Body-less SIP messages (180, ACK, empty 200) now always include the
mandatory
\r\n\r\nheader/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;--checklint 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. Plustest/condexecbranching,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’scall.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/-fdCSV, and-trace_msg/-trace_errfiles. - 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, honoringNO_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,
Makefileconvenience targets, CI (fmt + clippy + tests, and an interop job against real SIPp), and the Ferrous brand kit underbrand/.
Known limitations
- Signaling only over UDP. TCP/TLS,
-infinjection files (and thelookup/insert/replaceactions), 3PCC, RTP/pcap media, IPv6, and an HTTP control API are on the post-v1 roadmap. - The
eregregex engine is leftmost-first greedy (PCRE-style), not POSIX leftmost-longest — identical on the patterns real scenarios use; seedocs/SIPP_COMPAT.md§6.