xk6sip

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

xk6-sip

k6 extension k6/x/sip for load testing PBXs and SIP servers with scripted subscribers: each VU owns real SIP devices that register, call each other through the system under test and check what arrives on the other side.

Status: REGISTER, INVITE/CANCEL/BYE with digest auth; RTP with G.711 (PCMU/PCMA), RFC 3550 loss/jitter, RFC 4733 DTMF and audio detection; hold/resume, blind and attended transfer (REFER, Replaces), PRACK (100rel) and session timers.

Compatibility

xk6-sip k6 Go
v0.1.x – v0.3.x v2.x (built and tested with v2.3.0) 1.26+

Build and run

xk6 build v2.3.0 --with github.com/Dmitry-Fedotov-Dev/xk6-sip=. --output bin/k6
go build -o bin/testpbx ./cmd/testpbx

bin/testpbx -addr 127.0.0.1:5070 -users 200 -csv examples/subscribers.csv
bin/k6 run examples/call.js

Timings below a millisecond are only reliable on Linux: Go's clock on Windows ticks in ~0.5 ms steps.

Script API

import sip from 'k6/x/sip';

sip.options({ registerRate: 50, expectTimeout: '30s' }); // init context, optional

// No network in init; REGISTER is sent on first use and refreshed.
// Unknown string fields (ext, onk, gw_num, ...) are the device's numbers.
const ua1 = new sip.Device({ device: 'phone1', registrar: 'sip:pbx:5060',
  user: 'a@domain', pass: 'secret', expires: 180, ext: '701' });

export default function () {
  const out = ua1.call({ callee: ua2, aon: 'ext' });   // returns at once
  const inc = ua2.expectCall({ caller: ua1, aon: 'ext', timeout: '5s' }); // Call or false
  inc.accept();
  out.expectConnected();        // true/false
  inc.hangup();                 // CANCEL, reject or BYE depending on state
  out.expectDisconnected();
}

export function teardown() { sip.shutdown(); } // unregister everything

Only expect* methods and isHeard wait, so one VU can drive both ends of a call. They return false on timeout instead of throwing.

Full reference with every option and an example per method: docs/ (k6-docs format).

Module Description
new sip.Device(options) SIP subscriber; device, registrar, proxy, user, authUser, pass, expires, register, displayName, sessionExpires, prack, media options; any other field is a number (ext, onk...)
sip.options(options) localIP, registerRate, ringTimeout (3m), expectTimeout (30s), trace, deviceTag, media defaults
sip.audio(data), sip.tone(freq?, dbfs?) audio sources for calls
sip.shutdown() hang up, unregister and close all devices; call in teardown()
Device Description
call({callee, aon, timeout, id, headers, ...media}) send INVITE, return the Call at once (or false)
expectCall({caller, aon, timeout}) wait for a matching incoming call (or false)
register(), isRegistered() REGISTER now / last result
identity(key), id a number of the device / its name
destroy() hang up, unregister, close; next use starts it again
Call Description
accept(), reject(code?, reason?), hangup() answer, decline (603), end in any state
expectRinging/Connected/Disconnected(timeout?) wait for the state
state(), status(), remote(), callId, id calling/ringing/connected/ended, final status, other party
howCompleted() {endedBy, status, reason, duration} once ended
trace() SIP ladder of this leg
codec(), isHeard(timeout?), mediaStats() media checks and RTP statistics
sendDTMF(digits, ms?), expectDTMF(digits, timeout?), receivedDTMF() RFC 4733 DTMF
hold(), unhold(), isOnHold(), isRemoteHold() re-INVITE hold
transfer(dest, aon?), attendedTransfer(consult) REFER, REFER with Replaces
expectTransferred(timeout?), expectReferredCall(timeout?) transfer result / call placed on REFER
Debugging a call

call.trace() returns the SIP ladder of one call leg: every message the device sent (->) or received (<-) for this call, with the time since the first one. Print it when a check fails:

const out = ua1.call({ callee: '1999' });
if (!check(out, { 'connected': (c) => c.expectConnected('5s') })) {
  console.warn(out.trace());
}
+0.000s  -> INVITE sip:1999@test.local SIP/2.0
+0.001s  <- SIP/2.0 407 Proxy Authentication Required
+0.002s  -> INVITE (with credentials)
+0.003s  <- SIP/2.0 404 Not Found

Only start lines are kept, so tracing is cheap under load. sip.options({ trace: true }) keeps whole messages with headers and SDP. howCompleted() tells who ended the call and why, and callId finds the call in PBX logs.

Media

Every call sends RTP by default: a 1 kHz tone, so the far end can check that audio arrives. Streams are paced by a shared scheduler (500 VUs / ~1000 concurrent streams / 50k packets/s use ~0.6 CPU core on Linux).

const hello = sip.audio(open('./hello.wav', 'b')); // 16-bit PCM, 8 kHz, mono
const ua = new sip.Device({ ..., codecs: 'PCMA,PCMU', audio: hello });

const out = ua1.call({ callee: ua2, media: false }); // signalling only
inc.isHeard('3s');                // audio from the other side arrived
out.codec();                       // 'PCMA'
out.sendDTMF('1#');                // RFC 4733
inc.expectDTMF('1#', '5s');
out.mediaStats();                  // {codec, sent, received, lost, jitter, heard, dtmf}

Media options (media, codecs, audio: sip.audio(...) | sip.tone(freq, dbfs) | 'silence', heardLevel) can be set in sip.options(), per Device and per call.

Hold and transfer
inc.hold();                         // re-INVITE a=sendonly; out.isRemoteHold() === true
inc.unhold();

inc.transfer(ua3);                  // blind: REFER to ua3's ext (or '703', or (ua3, 'onk'))
inc.expectTransferred('10s');        // final NOTIFY says 2xx

const consult = ua2.call({ callee: ua3 }); /* ... answered ... */
inc.attendedTransfer(consult);      // REFER with Replaces

PBXs that handle REFER themselves (B2BUA, hosted PBX) keep the transferee's call and re-INVITE its media; when REFER reaches the endpoint instead, the device places the new call itself (call.expectReferredCall()) and answers INVITEs with Replaces automatically.

Device options: prack: true sends 180 reliably (RFC 3262) when the caller supports it (reliable 18x from the PBX are always PRACKed); sessionExpires: 1800 requests session timers (RFC 4028; refreshes and 422 are handled, a call whose peer stops refreshing is hung up).

Functional tests

examples/functional/ holds call-flow tests (basic call with DTMF, hold, blind and attended transfer) for one VU and one iteration: any failed step stops the scenario, k6 exits non-zero and writes a JUnit report.

bin/testpbx -addr 127.0.0.1:5070 -users 3
bin/k6 run -e JUNIT=report.xml examples/functional/attended-transfer.js
# against a real PBX:
bin/k6 run -e REGISTRAR=sip:pbx:5060 -e A_USER=701@pbx -e A_PASS=... -e A_EXT=701 ... examples/functional/hold.js

Metrics

metric type tags
sip_requests counter method, status
sip_request_duration trend method, status
sip_failed_requests rate method (401/407 challenges are not failures)
sip_call_setup_time trend INVITE → 200
sip_post_dial_delay trend INVITE → first 18x
sip_call_success rate status
sip_call_duration trend ended_by
sip_invite_delivery_time trend caller's INVITE → callee receives it
sip_registrations counter
sip_expect_failed counter expect
rtp_packets_sent / _received / _lost counter codec, direction (per call leg)
rtp_jitter trend RFC 3550 interarrival jitter per leg
rtp_audio_heard rate legs that received audio; < 1 means one-way audio

sip.options({ deviceTag: true }) adds a device tag to all of them.

Layout

  • engine/ – SIP core, independent of k6 (sipgo transactions + own dialog layer)
  • media/ – RTP: G.711, scheduler, statistics, DTMF, audio detection
  • testpbx/, cmd/testpbx – minimal registrar/B2BUA for tests and examples
  • repo root – the k6 adapter

Documentation

Overview

Package xk6sip is the k6 extension k6/x/sip. It exposes the SIP engine to k6 scripts and turns engine events into k6 metrics.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ModuleInstance

type ModuleInstance struct {
	// contains filtered or unexported fields
}

func (*ModuleInstance) Exports

func (mi *ModuleInstance) Exports() modules.Exports

type RootModule

type RootModule struct {
	// contains filtered or unexported fields
}

RootModule is shared by all VUs; it owns the single SIP engine of the process so that engine-wide limits (register rate) apply to the whole test.

func New

func New() *RootModule

func (*RootModule) NewModuleInstance

func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance

Directories

Path Synopsis
cmd
testpbx command
Command testpbx runs the test registrar/B2BUA for local k6 runs and writes the generated subscribers to a CSV file for the scripts.
Command testpbx runs the test registrar/B2BUA for local k6 runs and writes the generated subscribers to a CSV file for the scripts.
Package engine is the SIP core: subscribers (Device), calls and their timings.
Package engine is the SIP core: subscribers (Device), calls and their timings.
Package media is the RTP side of a call: G.711 audio sent from a shared scheduler, RFC 3550 receive statistics, RFC 4733 DTMF and detection of whether any audio arrives at all.
Package media is the RTP side of a call: G.711 audio sent from a shared scheduler, RFC 3550 receive statistics, RFC 4733 DTMF and detection of whether any audio arrives at all.
Package testpbx is a minimal registrar + B2BUA for tests and examples.
Package testpbx is a minimal registrar + B2BUA for tests and examples.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL