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 - 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
- High Performance: Built with Rust for maximum performance
- Easy to Use: Simple and intuitive API design
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.
- Transport support
- UDP
- TCP
- TLS
- WebSocket
- Digest Authentication
- Transaction Layer
- Dialog Layer
- WASM target
- no_std embassy-based targets
This SIP stack can be used in various scenarios, including but not limited to:
- Integration with WebRTC for browser-based communication, such as WebRTC SBC.
- Building custom SIP proxies or registrars
- Building custom SIP user agents (SIP.js alternative)
We are a group of developers who are passionate about SIP and Rust. We believe that Rust is a great language for building high-performance network applications, and we want to bring the power of Rust to the SIP/WebRTC/SFU world.
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.