A RFC 3261/3262 compliant SIP stack written in Rust. The goal of this project is to provide a high-performance, reliable, and easy-to-use SIP stack that can be used in various scenarios.
- RFC 3261/3262 Compliant: Full compliance with SIP specification
- Multiple Transport Support: UDP, TCP, TLS, WebSocket (TLS/WebSocket require the
rustlsandwebsocketfeatures, enabled by default). The TLS client goes through a backend seam (platform::tls) — host default is the built-in rustls; embedded stacks can register their own connector - IPv6 & Dual-Stack: listeners on
::serve IPv4 and IPv6; IPv4-mapped peers are canonicalized and UDP sends pick a reachable listener - Transaction Layer: Complete SIP transaction state machine
- Dialog Layer: SIP dialog management
- Reliable Provisionals: PRACK (RFC 3262 / 100rel) support
- Digest Authentication: Built-in authentication support
- no_std / Embedded: alloc-only core with an embassy backend
rsipstack compiles without std (alloc-only) for embassy-based targets —
verified on xtensa-esp32s3-none-elf (ESP32-S3):
cargo check --no-default-features --features platform-embassy-
Core SIP codec, transaction, dialog, and UDP transport layers are
no_std+alloc. -
The
platform-embassybackend maps the runtime seams ontoembassy-time/embassy-sync(critical-section based); task spawning is injected once at startup:rsipstack::platform::set_spawn_fn(|fut| spawner.spawn(fut).ok());
-
Concurrent waits use the backend-agnostic
select2/select3helpers instead oftokio::select!. -
SIPS (SIP over TLS, client role) goes through the
platform::tls::TlsConnectorseam: register a backend withplatform::tls::set_client_connectorand everyTlsConnection::connectrides it. Host builds without a registered connector keep using the built-in rustls path. -
TCP / WebSocket transports still require
platform-tokio; on embedded, use UDP or SIPS today. -
On no_std, register a
DomainResolverinstead of the tokio DNS resolver.
rsipstack follows RFC 3261 strictly. Stream transports require a valid single
Content-Length, Via received is bare for IPv6, dialogs follow target
refreshes, and transactions match per RFC. See the
CHANGELOG for release-specific behaviour changes.
rsipstack powers production SIP applications and related projects:
- RustPBX — programmable, software-defined PBX
- Active Call — SIP/WebRTC AI voice agent
- SipBot — SIP call-flow bot for testing and simulation
- RustRTC — WebRTC, RTP/SRTP and T.38 media stack used alongside rsipstack
- MiuTURN — TURN server for WebRTC
- Custom SIP proxies, registrars, user agents, and WebRTC SBCs
A stateful SIP proxy that routes calls between registered users:
# Run proxy server
cargo run --example proxy -- --port 25060 --addr 127.0.0.1
# Run with external IP
cargo run --example proxy -- --port 25060 --external-ip 1.2.3.4This example demonstrates:
- SIP user registration and location service
- Call routing between registered users
- Transaction forwarding and response handling
- Session management for active calls
- Handling INVITE, BYE, REGISTER, and ACK methods
A complete SIP client with registration, calling, and media support:
# Local demo proxy
cargo run --example client -- --port 25061 --sip-server 127.0.0.1:25060 --auto-answer
# Register with a SIP server
cargo run --example client -- --sip-server sip.example.com --user alice --password secret --auto-answeruse rsipstack::sip::{HostWithPort, Transport};
use rsipstack::transport::{udp::UdpConnection, SipAddr};
use tokio_util::sync::CancellationToken;
// Create UDP connection bound to an ephemeral local port
let cancel_token = CancellationToken::new();
let connection = UdpConnection::create_connection(
"0.0.0.0:0".parse()?,
None,
Some(cancel_token.child_token()),
)
.await?;
// Prepare the remote target
let target_addr = SipAddr::new(
Transport::Udp,
HostWithPort::try_from("127.0.0.1:5060")?,
);
// Send raw SIP message
let sip_message = "OPTIONS sip:test@example.com SIP/2.0\r\n...";
connection
.send_raw(sip_message.as_bytes(), &target_addr)
.await?;use rsipstack::transport::{
TcpListenerConnection, TransportLayer,
};
use tokio_util::sync::CancellationToken;
// Build a transport layer and register listeners
let cancel_token = CancellationToken::new();
let transport_layer = TransportLayer::new(cancel_token.clone());
let tcp_listener = TcpListenerConnection::new(
"0.0.0.0:5060".parse()?,
None,
)
.await?;
transport_layer.add_transport(tcp_listener.into());To add TLS or WebSocket listeners, construct a TlsListenerConnection or
WebSocketListenerConnection and register it with transport_layer.add_transport(...).
rustls crypto provider. rsipstack's TLS uses rustls's process-wide crypto provider and builds rustls with
ring. If your application also links theaws-lc-rsbackend (for example throughreqwest's default rustls setup), rustls cannot pick one on its own and the first TLS connection panics with "Could not automatically determine the process-level CryptoProvider". Install a provider once at startup, before creating any TLS transport:rustls::crypto::ring::default_provider() .install_default() .expect("install rustls crypto provider"); // or: rustls::crypto::aws_lc_rs::default_provider().install_default()The installed provider is used for every rustls user in the process, rsipstack included, so it is also where to restrict cipher suites.
The raw TransportEvent stream is an internal API consumed by Endpoint::serve.
To process incoming SIP messages, build an Endpoint around the transport layer and
drive it with endpoint.serve() while consuming transactions via
endpoint.incoming_transactions() (see the next section).
use rsipstack::sip::{Method, StatusCode};
use rsipstack::{EndpointBuilder, transport::TransportLayer};
use tokio_util::sync::CancellationToken;
// Build endpoint with transport layer
let cancel_token = CancellationToken::new();
let transport_layer = TransportLayer::new(cancel_token.clone());
let endpoint = EndpointBuilder::new()
.with_transport_layer(transport_layer)
.with_cancel_token(cancel_token.clone())
.build();
// Start endpoint background task
let endpoint_inner = endpoint.inner.clone();
tokio::spawn(async move {
if let Err(err) = endpoint_inner.serve().await {
eprintln!("endpoint stopped: {err}");
}
});
// Handle incoming transactions
let mut incoming = endpoint
.incoming_transactions()
.expect("transaction receiver available");
while let Some(transaction) = incoming.recv().await {
// Process transaction based on method
match transaction.original.method {
Method::Register => {
transaction.reply(StatusCode::OK).await?;
}
Method::Options => {
transaction.reply(StatusCode::OK).await?;
}
// ... handle other methods
}
}use rsipstack::dialog::dialog_layer::DialogLayer;
use rsipstack::dialog::registration::Registration;
use rsipstack::dialog::authenticate::Credential;
use rsipstack::dialog::invitation::InviteOption;
use std::sync::Arc;
// Create dialog layer
let dialog_layer = Arc::new(DialogLayer::new(endpoint.inner.clone()));
// Register with server
let credential = Credential {
username: "alice".to_string(),
password: "secret".to_string(),
realm: None,
auth_username: None,
};
let mut registration = Registration::new(endpoint.inner.clone(), Some(credential.clone()));
let response = registration.register("sip:registrar.example.com".parse()?, None).await?;
// Make outgoing call
let invite_option = InviteOption {
callee: "sip:bob@example.com".parse()?,
caller: "sip:alice@example.com".parse()?,
content_type: None,
offer: None,
contact: "sip:alice@192.168.1.100:5060".parse()?,
credential: Some(credential),
headers: None,
..Default::default()
};
// Create the dialog state channel (drives `process_dialog`-style loops)
let (state_sender, _state_receiver) = dialog_layer.new_dialog_state_channel();
let (invite_dialog, response) = dialog_layer.do_invite(invite_option, state_sender).await?;use rsipstack::sip::{Method, StatusCode};
use rsipstack::transaction::{Transaction, key::{TransactionKey, TransactionRole}};
use rsipstack::sip::prelude::HeadersExt;
use std::collections::HashMap;
// Handle incoming requests
while let Some(mut transaction) = incoming.recv().await {
match transaction.original.method {
Method::Register => {
// Store user registration
let user = User::try_from(&transaction.original)?;
users.insert(user.username.clone(), user);
transaction.reply(StatusCode::OK).await?;
}
Method::Invite => {
// Route call to registered user
let callee = transaction.original.to_header()?.uri()?.auth
.map(|a| a.user)
.unwrap_or_default();
if let Some(target) = users.get(&callee) {
// Create new client transaction for forwarding
let mut forwarded_req = transaction.original.clone();
let via = transaction.endpoint_inner.get_via(None, None)?;
forwarded_req.headers.push_front(via.into());
let key = TransactionKey::from_request(&forwarded_req, TransactionRole::Client)?;
let mut forwarded_tx = Transaction::new_client(
key,
forwarded_req,
transaction.endpoint_inner.clone(),
None
);
forwarded_tx.destination = Some(target.destination.clone());
forwarded_tx.send().await?;
} else {
transaction.reply(StatusCode::NotFound).await?;
}
}
// ... handle other methods
}
}This is a simplified sketch: User, users and incoming are types/values you define yourself
(incoming comes from endpoint.incoming_transactions()?, and User is a struct you implement
that holds the registered user's username and destination SipAddr). A complete, working proxy
is provided in examples/proxy.rs.
cargo test# Run server
cargo run -r --bin bench_ua -- -m server -p 5060
# Run client with 1000 calls
cargo run -r --bin bench_ua -- -m client -p 5061 -s 127.0.0.1:5060 -c 1000The test monitor:
=== SIP Benchmark UA Stats ===
Dialogs: 9992
Active Calls: 9983
Rejected Calls: 0
Failed Calls: 0
Total Calls: 250276
Calls/Second: 1501
============================We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.