Skip to content

SIP client stack · softphones · voice agents

SIP in Rust.
One C ABI.
No GPL code inside.

sipral Session Initiation Protocol Rust Audio Layer

A library that makes and answers phone calls over the internet: calls, audio, encryption and getting through home and office routers. Use it in place of PJSIP without the GPL, or plug a phone line straight into a voice agent.

concurrent calls in one process
10 000
Audio both ways on every call, set up at about 500 a second.
of memory per call
72 KB
A softphone barely notices it; a server holds thousands of calls.
fuzzing executions
61 billion
Hostile input thrown at the parsers and protocol engines. Zero crashes.
languages, one interface
8
Swift, Kotlin, Java, .NET, Python, Dart, React Native and Node.js, over one C ABI.

01 The API

A call, in Python.

No bind address, no media address: the stack listens on every interface and advertises the route toward the server it found by RFC 3263.

With audio=AudioMode.APPLICATION the same call hands its audio over instead: call.media.frames yields the far end as 16-bit PCM, and send_audio(pcm) is what the far end hears. That is all a voice agent needs.

Swift, Kotlin, .NET, Dart, React Native and Node.js have the same shape.

Every language
call.py python
import asyncio
from sipral import Stack
from sipral.enums import RegistrationState

async def main():
    with Stack(loop=asyncio.get_running_loop()) as stack:  # device mode: the library opens mic and speaker
        account = stack.add_account(
            "sip:alice@example.com", registrar="sip:example.com",
            server_uri="sip:example.com",  # located by RFC 3263: NAPTR, SRV, A/AAAA
            auth_user="alice", auth_password="secret")
        account.register()
        while account.registration_state != RegistrationState.REGISTERED:
            await stack.events.get()
        call = stack.place_call(account, "sip:bob@example.com")
        while not call.ended:
            print((await call.events.get()).kind_name)

asyncio.run(main())
A complete call, from registration to hangup, against the real library.

02 Why teams choose it

What it changes for your product.

Each one is checked by the build or tested in the lab, not just promised.
  1. 01

    Fewer crashes, fewer CVEs

    Written in Rust, so whole classes of memory bugs cannot happen. The few unsafe lines sit only where it talks to C and to the sound card, and code that could crash at runtime fails the build.

    61 billion fuzzing executions, zero crashes

  2. 02

    Closed source and app stores

    No GPL or LGPL code anywhere in it: written clean-room from the RFCs, with permissively licensed dependencies only. Under the commercial licence it ships in closed products and through the app stores.

    AGPL-3.0 or a commercial licence

  3. 03

    One integration, every platform

    One C ABI, with the Swift, Kotlin, .NET, Python, Dart and Node.js bindings generated from it, plus Java, .NET MAUI and React Native on top. iOS, Android, macOS, Windows and Linux from one codebase.

    layouts verified on 64-bit and both 32-bit ABIs

  4. 04

    Voice agents, no media server

    Each call hands what the caller says to your agent as plain audio, 20 ms at a time, and plays back whatever the agent says. The agent can live in the same program or in a separate one, or run entirely on your machine with no key.

    application mode on every platform and language

  5. 05

    A softphone with no audio code

    Device mode opens the microphone and speaker itself, with the platform echo cancellation: CoreAudio on Apple platforms, WASAPI on Windows, AAudio on Android.

    CallKit, PushKit and ConnectionService helpers

  6. 06

    Encrypted the way carriers ask

    SRTP with SDES and AES-GCM, DTLS-SRTP, TLS with the platform trust store, a private CA or a pinned certificate, and STIR/SHAKEN to sign calls and verify callers.

    SRTP best effort for PBXs that refuse encrypted offers

  7. 07

    Calls that survive real networks

    STUN with failover, TURN over UDP, TCP and TLS, ICE in the full and lite roles, keep-alives that hold NAT bindings open, and a call that follows its own address when the phone changes networks.

    RFC 8489 · 8656 · 8445 · 5626

  8. 08

    The PBXs your customers run

    Tested against Asterisk, FreeSWITCH, FusionPBX, Kamailio, OpenSIPS, baresip, FreePBX 16 and 17, VitalPBX and a VICIdial dialer, on good and deliberately bad networks.

    calls, transfers, DTMF, SRTP, DTLS-SRTP, ICE, TURN

  9. 09

    Measured, not claimed

    Ten thousand concurrent calls in one process at about 72 KB each. The least memory and the least CPU per call, measured against PJSIP, baresip and Linphone.

    The measured comparison

03 Architecture

The two open cells are the core.

The heart of the stack never touches the network or the clock. Your code hands it bytes and the time; it hands back bytes to send and events to act on. That makes every case repeatable in a test.

In the mark, seven filled cells trace the S. The two left open are the part of the stack that opens no sockets. The red one is the active call.

the core's surface, in essence rust
fn receive(&mut self, input: Input<'_>, now: Instant) -> Result<(), ReceiveError>;
fn handle_timeout(&mut self, now: Instant);
fn poll_transmit(&mut self) -> Option<Transmit>;
fn poll_event(&mut self) -> Option<Event>;
fn poll_timeout(&self) -> Option<Instant>;
  1. i.

    Fewer bugs in the field.

    Every state machine is tested without a network: a CANCEL racing a 200 OK is reproduced in a unit test, not on a customer's carrier.

  2. ii.

    It fits your architecture.

    It embeds in Swift concurrency, a .NET Task, Kotlin coroutines, Tokio or a bare event loop.

  3. iii.

    It never blocks your app.

    There is no socket inside the core to block on.

  4. iv.

    It ships lean.

    Audio devices and TLS stay at the edges, and a headless build does not link them.

04 On the wire

Every call, verified message by message.

The load test runs this exchange for a thousand calls at once, and fails if a single message goes missing or a single call ends the wrong way.

RFC 3263
NAPTR, SRV, A/AAAA; failover on timeout, lost transport or 503; re-lookup on TTL
RFC 3261 §18.1.1
a request over 1300 bytes is not put in a datagram: TCP, or a trimmed retry
RFC 3262 · 3311
reliable provisional responses with PRACK, and UPDATE
RFC 4028
session timers
RFC 5626 · 5627
Outbound keep-alives and GRUU
RFC 8599
calls announced by push notification
sipral PBX
  1. INVITE
  2. 401 Unauthorized
  3. ACK
  4. INVITE
  5. 100 Trying
  6. 180 Ringing
  7. PRACK
  8. 200 OK
  9. 200 OK
  10. ACK
  11. RTP or SRTP, audio both ways
  12. re-INVITE
  13. BYE
  14. 200 OK
The call: from Sipral: INVITE (SDP offer); from the PBX: 401 Unauthorized (digest challenge); from Sipral: ACK; from Sipral: INVITE (with credentials); from the PBX: 100 Trying; from the PBX: 180 Ringing (reliable, RFC 3262); from Sipral: PRACK; from the PBX: 200 OK (PRACK); from the PBX: 200 OK (SDP answer); from Sipral: ACK; media: RTP or SRTP, audio both ways; from Sipral: re-INVITE (hold, then resume); from Sipral: BYE; from the PBX: 200 OK.

05 Interoperability

Proven against the PBXs your customers run.

Every scenario runs against the phone systems companies actually use, on good and deliberately bad networks, and against FreePBX 16 and 17, VitalPBX, and a VICIdial dialer ringing an agent's phone into its session.
Flows passed against each peer in the most recent lab run
Peer Register Call Hold, resume Blind transfer Attended transfer DTMF, RFC 4733 DTMF, SIP INFO SRTP, SDES DTLS-SRTP Compact form G.729 MESSAGE MWI
Asterisk 22.10.1 the configuration most integrators run
FreeSWITCH via Kamailio 1.10.12 / 6.1.4 registration and routing through a proxy
FreeSWITCH via OpenSIPS 1.10.12 / 4.0.2 a second, independent proxy
baresip via Kamailio 4.11.0 / 6.1.4 phone to phone, end to end
FreePBX 17 17 (Asterisk 22.10.1, Debian 12) its Asterisk on the lab's flows, beside its own extensions
FreePBX 16 1 16 (Asterisk 18.9-cert13) its Asterisk on the lab's flows, beside its own extensions
FusionPBX 5.6.5 (FreeSWITCH 1.10.12) its own database, profiles and dialplan
VitalPBX 2 4.5 (Asterisk 20) a running system, on two test tenants
VICIdial ViciBox 12 (Asterisk 18.26) its Asterisk on the lab's flows, and an agent's phone rung into its session
passed not part of this suite
  1. 1 This FreePBX 16 runs on a system with OpenSSL 1.0.2, which predates the extended master secret (RFC 7627). Sipral does not key DTLS-SRTP without it, as RFC 7627 §5.4 asks; on FreePBX 17, with OpenSSL 3, it passes.
  2. 2 VitalPBX ties each mailbox to the extension, refuses a subscription to it and sends message waiting unasked; Sipral answers a NOTIFY outside a subscription 481 (RFC 6665 §4.1.3), so MWI does not pass there yet. The test tenants offer PCMA alone and no media encryption, so SRTP, DTLS-SRTP and G.729 are not tried.
Asterisk 22.10.1, from behind a NAT
calling and called through STUN, and still reachable minutes after registering: keep-alives hold the NAT open
Sipral to Sipral, each behind its own NAT
full ICE through two NATs, relayed through TURN where the NATs block a direct path, forked calls relayed to two phones, STUN failover past a dead server
Headless agent, ICE-lite
ICE required by the far end, through the native API and the C interface
Two Sipral stacks, C ABI to C ABI
STIR/SHAKEN: signed calls verified and carried, unsigned calls and untrusted signatures refused

61 billion

fuzzing executions

Malformed, hostile and random input against the parsers and protocol engines. Zero crashes, zero hangs.

RFC 4475

the SIP torture suite

Every message of the IETF's torture tests parsed and asserted.

Rust

memory-safe throughout

Unsafe code confined to the C boundary and the audio device layer.

06 Compared with PJSIP, baresip and Linphone

Less memory. Less CPU. An INVITE that always gets through.

Four SIP clients against the same Asterisk, each the build its distribution ships: timed from packet captures, measured from the same /proc files.

5.8 MB registered and idle, where the others need 12 to 58 MB. A fifth of baresip's CPU per call at a hundred calls, a sixth of pjsua's over three hours of calls. The only authenticated ICE INVITE that crosses UDP whole, and audio back in well under a second after the network changes.

Every measured table
Sipral pjsua baresip linphonec lower is better
  • Memory, registered and idle

    Sipral
    5.8 MB pjsua
    12.7 MB baresip
    12.2 MB linphonec
    57.7 MB
  • Processor time per call, three hours of three-minute calls

    Sipral
    1.77 s pjsua
    10.46 s baresip
    4.08 s linphonec
    2.84 s
  • The INVITE that sets up an ICE call

    Sipral
    1236 B pjsua
    1869 B fragmented baresip
    1479 B fragmented, one run of two linphonec
    1594 B fragmented
  • Audio back after the network changed

    Sipral
    640 ms pjsua
    never baresip
    2200 ms linphonec
    2100 ms

How this was measured

07 One C ABI

Five platforms, one header, every binding printed from it.

The C header and the Swift, Kotlin, .NET, Python, Dart and Node.js bindings are generated from one source. If a function is missing from one language, the build fails before anyone ships it.
sipral.h, excerpt c
#define SIPRAL_ABI_VERSION_MAJOR ((uint32_t)1)
#define SIPRAL_ABI_VERSION_MINOR ((uint32_t)2)
typedef uint64_t sipral_handle_t;

sipral_status_t sipral_stack_create(const sipral_stack_config_t *config, sipral_handle_t *out_stack);
sipral_status_t sipral_stack_poll(sipral_handle_t stack, uint64_t now_ms, sipral_poll_result_t *result);
sipral_status_t sipral_account_add(sipral_handle_t stack, const sipral_account_config_t *config, sipral_handle_t *out_account);
sipral_status_t sipral_account_register(sipral_handle_t stack, sipral_handle_t account, uint64_t now_ms);
sipral_status_t sipral_call_place(sipral_handle_t stack, sipral_handle_t account, const sipral_call_config_t *config, sipral_handle_t *out_call, uint64_t now_ms);
sipral_status_t sipral_media_receive(sipral_handle_t media, uint8_t *data, size_t len, const char *from, size_t from_len, uint64_t now_ms, sipral_arrival_t *out_arrival);
sipral_status_t sipral_media_playback(sipral_handle_t media, int16_t *samples, size_t capacity, size_t *out_written, sipral_playback_t *out_source);
sipral_status_t sipral_call_hangup(sipral_handle_t stack, sipral_handle_t call, uint64_t now_ms);
Integrate once: an application built against C ABI 1.0 runs on every 1.x library.
Platforms, languages and device mode
Platform Languages
macOS RustCSwiftKotlin/Java.NETPythonDartNode.js
iOS RustCSwift.NET MAUIReact NativeFlutter
Android RustCKotlin.NET MAUIReact NativeFlutter
Windows RustC.NETPythonDartNode.js
Linux RustCKotlin/Java.NETPythonDartNode.js

Application mode, PCM in the application's hands, is available on every platform and in every language.

Platforms, artefacts and binding examples

08 Licence

Pick the arm that fits what you ship.

There is no licence check inside the library. The commercial arm is a contract, not a runtime lock.

AGPL-3.0-only

Free

  • Open source apps that are themselves copyleft
  • Research, teaching, evaluation, prototypes you do not ship
  • Network use of a modified Sipral, with your source published

LicenseRef-Sipral-Commercial

Commercial

  • Closed-source desktop, mobile and server products
  • App Store and Google Play distribution
  • One price per company, no royalties, no per-seat or per-channel counting
Request a quote
Which one you need, in one page

09 Roadmap

1.2 is out. Here is what comes next.

Every 1.x release only adds to the C ABI fixed at 1.0, so an application built on 1.0 still runs on 1.2. Commercial licences are available now.
  1. October 2026 · 1.2 (released)

    Voice agents, the enterprise and more platforms

    Connectors for the services voice agents run on, OAuth2 sign-in to the PBX, Node.js and .NET MAUI packages, a network test before the call, answering-machine detection, SIP over WebSocket and LiveCommunicationKit on iOS.

  2. November 2026 · 1.3 In progress (planned, not done)

    On real phones, and on WebRTC lines

    The iOS and Android packages proven on real phones and through the test tracks of both stores, and calls on the WebRTC lines PBXs offer: one port for the whole call, in the profile browsers speak.

  3. December 2026 · 1.4 In progress (planned, not done)

    Browsers and the telephone network

    Trickle ICE, an audio call to and from a browser, SIP-T and SIP-I through a carrier gateway, and C, C++ and Rust applications that no longer write their own networking.

  4. March 2027 · 1.5 (planned, not done)

    Keys that never leave the device

    A client certificate on SIP over TLS, the private key held and used by the platform security hardware, on phones and on the desktop alike.

10 Questions

Asked before buying.

Something not here? Ask through the form.

What is Sipral?

Sipral (Session Initiation Protocol Rust Audio Layer) is a SIP client stack written in Rust: signalling, media, encryption and NAT traversal behind one C ABI. It is meant to replace PJSIP in softphones and to put a phone line inside a voice agent without a media server in between.

Can Sipral replace PJSIP in an existing softphone?

That is what it is built for. It covers registration, calls, hold, transfers, DTMF, SRTP, DTLS-SRTP, ICE and TURN, and its documentation includes a migration guide that maps each pjsua concept to its equivalent in Sipral. It contains no GPL or LGPL code, so a closed-source product does not inherit GPL obligations from it.

Does Sipral contain any GPL or LGPL code?

No. It was written clean-room from the RFCs and ITU Recommendations, and it links only permissively licensed dependencies (MIT, BSD, Apache-2.0, ISC, Zlib and equivalents). The build fails if a dependency under any other licence is added.

Which platforms and languages does Sipral support?

macOS, iOS, Android, Windows and Linux. Besides Rust and C, it has bindings for Swift, Kotlin and Java, .NET and .NET MAUI, Python, Dart and Flutter, React Native, and Node.js with TypeScript. The C header and the Swift, Kotlin, .NET, Python, Dart and Node.js bindings are generated from one declaration of the C ABI.

Can Sipral run without a microphone or speaker, for a voice agent?

Yes. Next to device mode, where the library opens the microphone and speaker itself, application mode hands every call's audio to the application as 16-bit PCM, one frame at a time, and takes what the far end should hear back the same way. A headless build can also carry that audio over a socket to a separate agent process.

Which encryption does Sipral support?

SRTP keyed by SDES (AES-CM 128 and 256, AES-GCM), DTLS-SRTP written in-tree, an SRTP best-effort mode for PBXs that refuse encrypted offers, TLS with the platform trust store, a private CA or one pinned certificate, and STIR/SHAKEN signing and verification (RFC 8224, RFC 8588).

How does Sipral get through NAT?

STUN with failover, TURN over UDP, TCP and TLS, ICE in the full and lite roles with restarts and consent freshness, RFC 5626 keep-alives, and a call that follows its own address when the network changes.

Which version is current?

Sipral 1.2, at C ABI 1.2. An application built against 1.0 or 1.1 keeps working with every later 1.x library, without a rebuild. The packages are on PyPI, npm, NuGet, Maven Central, pub.dev and crates.io, the Swift package is the GitHub repository, and commercial licences are available through the form.

Do I need a commercial licence to ship Sipral in an App Store app?

Yes. Under the AGPL-3.0 the whole application must reach its users with its source, which the terms of the App Store and Google Play conflict with. The commercial licence allows closed-source distribution and app stores explicitly, with no royalties and no per-seat or per-channel counting.

How is Sipral tested?

With 61 billion fuzzing executions against its parsers and protocol engines and zero crashes, with every message of the RFC 4475 SIP torture suite asserted, and with a lab that runs calls, transfers, DTMF, SRTP, DTLS-SRTP, NAT, ICE and TURN against Asterisk, FreeSWITCH, FusionPBX, Kamailio, OpenSIPS, baresip, FreePBX 16 and 17, VitalPBX and a VICIdial dialer, through the native API and again through the C interface.

Ship SIP in a closed app, without the GPL.

One price per company, perpetual for the versions delivered during maintenance, app stores allowed. Ask privately.

Request a quote