use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use rsipstack::dialog::authenticate::Credential;
use rsipstack::dialog::dialog::{DialogState, DialogStateReceiver, DialogStateSender};
use rsipstack::dialog::dialog_layer::DialogLayer;
use rsipstack::dialog::invitation::InviteOption;
use rsipstack::dialog::server_dialog::ServerInviteDialog;
use rsipstack::sip as rsip;
use rsipstack::sip::prelude::HeadersExt;
use rsipstack::transaction::endpoint::EndpointInnerRef;
use rsipstack::transaction::TransactionReceiver;
use rsipstack::transport::{udp::UdpConnection, TransportLayer};
use rsipstack::EndpointBuilder;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, Mutex};
use tokio_util::sync::CancellationToken;
use crate::error::FlowcatError;
use crate::sip::sdp;
use crate::sip::transport::SipTransport;
const DEFAULT_SIP_PORT: u16 = 5060;
pub const DEFAULT_RTP_PORT_BASE: u16 = 16000;
pub const DEFAULT_RTP_PORT_TRIES: u16 = 200;
const MIN_REREGISTER_SECS: u64 = 30;
#[derive(Debug, Clone)]
pub struct SipConfig {
pub server: String,
pub login: String,
pub password: String,
pub caller_id: String,
pub public_ip: Option<Ipv4Addr>,
pub sip_port: Option<u16>,
pub rtp_port_base: u16,
pub rtp_port_tries: u16,
}
pub struct InboundInvite {
pub call_id: String,
pub from: String,
pub to_did: String,
offer: sdp::SdpMedia,
dialog: ServerInviteDialog,
advertise_ip: Ipv4Addr,
rtp_port_base: u16,
rtp_port_tries: u16,
state_rx: DialogStateReceiver,
}
impl InboundInvite {
pub async fn answer(self) -> Result<SipTransport, FlowcatError> {
let codec = self.offer.codec;
let (rtp_sock, rtp_port) = bind_rtp_socket(self.rtp_port_base, self.rtp_port_tries).await?;
let answer_sdp = sdp::build_answer(self.advertise_ip, rtp_port, codec);
let peer = SocketAddr::new(IpAddr::V4(self.offer.ip), self.offer.port);
let headers = vec![rsip::Header::ContentType("application/sdp".into())];
self.dialog
.accept(Some(headers), Some(answer_sdp.into_bytes()))
.map_err(|e| FlowcatError::Transport(format!("SIP accept failed: {e}")))?;
let hangup = CancellationToken::new();
spawn_dialog_watch(self.state_rx, hangup.clone());
Ok(SipTransport::start(
rtp_sock,
peer,
codec,
self.call_id,
hangup,
))
}
pub fn reject(self, code: Option<rsip::StatusCode>) {
let _ = self
.dialog
.reject(Some(code.unwrap_or(rsip::StatusCode::BusyHere)), None);
}
}
pub struct SipAgent {
cfg: SipConfig,
endpoint_inner: EndpointInnerRef,
dialog_layer: Arc<DialogLayer>,
inbound_rx: Mutex<mpsc::Receiver<InboundInvite>>,
advertise_ip: Ipv4Addr,
cancel: CancellationToken,
}
impl SipAgent {
pub async fn start(cfg: SipConfig) -> Result<Self, FlowcatError> {
let cancel = CancellationToken::new();
let sip_port = cfg.sip_port.unwrap_or(DEFAULT_SIP_PORT);
let local: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), sip_port);
let external = cfg
.public_ip
.map(|ip| SocketAddr::new(IpAddr::V4(ip), sip_port));
let transport_layer = TransportLayer::new(cancel.clone());
let conn = UdpConnection::create_connection(local, external, Some(cancel.child_token()))
.await
.map_err(|e| FlowcatError::Transport(format!("bind SIP UDP {local}: {e}")))?;
transport_layer.add_transport(conn.into());
let endpoint = EndpointBuilder::new()
.with_user_agent("flowcat")
.with_cancel_token(cancel.clone())
.with_transport_layer(transport_layer)
.build();
let endpoint_inner = endpoint.inner.clone();
let dialog_layer = Arc::new(DialogLayer::new(endpoint.inner.clone()));
let advertise_ip = match cfg.public_ip {
Some(ip) => ip,
None => local_advertise_ip(&endpoint),
};
let incoming = endpoint
.incoming_transactions()
.map_err(|e| FlowcatError::Transport(format!("incoming_transactions: {e}")))?;
let serve_cancel = cancel.clone();
tokio::spawn(async move {
tokio::select! {
_ = endpoint.serve() => {}
_ = serve_cancel.cancelled() => {}
}
});
let (inbound_tx, inbound_rx) = mpsc::channel::<InboundInvite>(16);
tokio::spawn(incoming_pump(
dialog_layer.clone(),
incoming,
inbound_tx,
advertise_ip,
cfg.rtp_port_base,
cfg.rtp_port_tries,
cancel.clone(),
));
if !cfg.password.is_empty() {
tokio::spawn(register_loop(
endpoint_inner.clone(),
cfg.clone(),
cancel.clone(),
));
}
Ok(Self {
cfg,
endpoint_inner,
dialog_layer,
inbound_rx: Mutex::new(inbound_rx),
advertise_ip,
cancel,
})
}
pub async fn register(&self) -> Result<u32, FlowcatError> {
let credential = Credential {
username: self.cfg.login.clone(),
password: self.cfg.password.clone(),
realm: None,
};
let server = parse_server_uri(&self.cfg.server)?;
let mut reg = rsipstack::dialog::registration::Registration::new(
self.endpoint_inner.clone(),
Some(credential),
);
let resp = reg
.register(server, None)
.await
.map_err(|e| FlowcatError::Transport(format!("REGISTER failed: {e}")))?;
if resp.status_code != rsip::StatusCode::OK {
return Err(FlowcatError::Transport(format!(
"REGISTER rejected: {}",
resp.status_code
)));
}
Ok(reg.expires())
}
pub async fn next_inbound(&self) -> Option<InboundInvite> {
self.inbound_rx.lock().await.recv().await
}
pub async fn originate(
&self,
to_e164: &str,
caller_id: Option<&str>,
) -> Result<SipTransport, FlowcatError> {
let server = parse_server_uri(&self.cfg.server)?;
let host = server.host_with_port.clone();
let caller_user = caller_id.unwrap_or(&self.cfg.caller_id);
let caller = make_uri(caller_user, host.clone());
let callee = make_uri(to_e164, host.clone());
let contact = make_uri(caller_user, host.clone());
let (rtp_sock, rtp_port) =
bind_rtp_socket(self.cfg.rtp_port_base, self.cfg.rtp_port_tries).await?;
let offer = sdp::build_offer(self.advertise_ip, rtp_port);
let credential = Credential {
username: self.cfg.login.clone(),
password: self.cfg.password.clone(),
realm: None,
};
let invite = InviteOption {
caller,
callee,
contact,
content_type: Some("application/sdp".to_string()),
offer: Some(offer.into_bytes()),
credential: Some(credential),
..Default::default()
};
let (state_tx, state_rx) = self.dialog_layer.new_dialog_state_channel();
let (dialog, resp) = self
.dialog_layer
.do_invite(invite, state_tx)
.await
.map_err(|e| FlowcatError::Transport(format!("INVITE failed: {e}")))?;
let resp =
resp.ok_or_else(|| FlowcatError::Transport("INVITE got no final response".into()))?;
if resp.status_code != rsip::StatusCode::OK {
return Err(FlowcatError::Transport(format!(
"outbound call not answered: {}",
resp.status_code
)));
}
let answer_body = String::from_utf8_lossy(resp.body());
let media = sdp::parse(&answer_body)
.map_err(|e| FlowcatError::Transport(format!("bad answer SDP: {e}")))?;
let peer = SocketAddr::new(IpAddr::V4(media.ip), media.port);
let call_id = dialog.id().call_id.to_string();
let hangup = CancellationToken::new();
spawn_dialog_watch(state_rx, hangup.clone());
Ok(SipTransport::start(
rtp_sock,
peer,
media.codec,
call_id,
hangup,
))
}
pub fn shutdown(&self) {
self.cancel.cancel();
}
}
impl Drop for SipAgent {
fn drop(&mut self) {
self.cancel.cancel();
}
}
fn spawn_dialog_watch(mut state_rx: DialogStateReceiver, hangup: CancellationToken) {
tokio::spawn(async move {
while let Some(state) = state_rx.recv().await {
if let DialogState::Terminated(id, reason) = state {
tracing::debug!(%id, ?reason, "SIP dialog terminated");
hangup.cancel();
break;
}
}
hangup.cancel();
});
}
async fn incoming_pump(
dialog_layer: Arc<DialogLayer>,
mut incoming: TransactionReceiver,
inbound_tx: mpsc::Sender<InboundInvite>,
advertise_ip: Ipv4Addr,
rtp_port_base: u16,
rtp_port_tries: u16,
cancel: CancellationToken,
) {
loop {
let mut tx = tokio::select! {
_ = cancel.cancelled() => break,
t = incoming.recv() => match t {
Some(t) => t,
None => break,
},
};
let has_to_tag = tx
.original
.to_header()
.ok()
.and_then(|to| to.tag().ok().flatten())
.is_some();
if has_to_tag {
if let Some(mut d) = dialog_layer.match_dialog(&tx) {
tokio::spawn(async move {
let _ = d.handle(&mut tx).await;
});
} else {
let _ = tx
.reply(rsip::StatusCode::CallTransactionDoesNotExist)
.await;
}
continue;
}
match tx.original.method {
rsip::Method::Invite => {
if let Err(e) = handle_new_invite(
&dialog_layer,
tx,
&inbound_tx,
advertise_ip,
rtp_port_base,
rtp_port_tries,
)
.await
{
tracing::debug!(error = %e, "failed to set up inbound INVITE");
}
}
rsip::Method::Ack => { }
_ => {
let _ = tx.reply(rsip::StatusCode::OK).await;
}
}
}
}
async fn handle_new_invite(
dialog_layer: &Arc<DialogLayer>,
tx: rsipstack::transaction::transaction::Transaction,
inbound_tx: &mpsc::Sender<InboundInvite>,
advertise_ip: Ipv4Addr,
rtp_port_base: u16,
rtp_port_tries: u16,
) -> Result<(), FlowcatError> {
let mut tx = tx;
let from = uri_user(tx.original.from_header().ok().and_then(|h| h.uri().ok()));
let to_did = uri_user(tx.original.to_header().ok().and_then(|h| h.uri().ok()));
let call_id = tx
.original
.call_id_header()
.map(|c| c.to_string())
.unwrap_or_default();
let offer_body = String::from_utf8_lossy(tx.original.body());
let offer = match sdp::parse(&offer_body) {
Ok(m) => m,
Err(e) => {
tracing::debug!(error = %e, "rejecting INVITE with unusable SDP offer");
let _ = tx.reply(rsip::StatusCode::NotAcceptableHere).await;
return Err(FlowcatError::Transport(format!("bad offer SDP: {e}")));
}
};
let (state_tx, state_rx): (DialogStateSender, DialogStateReceiver) =
dialog_layer.new_dialog_state_channel();
let dialog = dialog_layer
.get_or_create_server_invite(&tx, state_tx, None, None)
.map_err(|e| FlowcatError::Transport(format!("get_or_create_server_invite: {e}")))?;
let mut dialog_for_handle = dialog.clone();
tokio::spawn(async move {
let _ = dialog_for_handle.handle(&mut tx).await;
});
let invite = InboundInvite {
call_id,
from,
to_did,
offer,
dialog,
advertise_ip,
rtp_port_base,
rtp_port_tries,
state_rx,
};
inbound_tx
.send(invite)
.await
.map_err(|_| FlowcatError::Transport("inbound channel closed".into()))
}
async fn register_loop(
endpoint_inner: EndpointInnerRef,
cfg: SipConfig,
cancel: CancellationToken,
) {
let credential = Credential {
username: cfg.login.clone(),
password: cfg.password.clone(),
realm: None,
};
let server = match parse_server_uri(&cfg.server) {
Ok(u) => u,
Err(e) => {
tracing::error!(error = %e, "SIP register loop: bad server URI; not registering");
return;
}
};
let mut reg =
rsipstack::dialog::registration::Registration::new(endpoint_inner, Some(credential));
loop {
match reg.register(server.clone(), None).await {
Ok(resp) if resp.status_code == rsip::StatusCode::OK => {
let expires = reg.expires();
tracing::info!(
expires = (expires as u64).max(MIN_REREGISTER_SECS),
"SIP registered"
);
tokio::select! {
_ = cancel.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(reregister_delay_secs(expires))) => {}
}
}
Ok(resp) => {
tracing::warn!(status = %resp.status_code, "SIP register rejected; retrying");
tokio::select! {
_ = cancel.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(MIN_REREGISTER_SECS)) => {}
}
}
Err(e) => {
tracing::warn!(error = %e, "SIP register error; retrying");
tokio::select! {
_ = cancel.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(MIN_REREGISTER_SECS)) => {}
}
}
}
}
}
fn parse_server_uri(server: &str) -> Result<rsip::Uri, FlowcatError> {
let s = if server.starts_with("sip:") || server.starts_with("sips:") {
server.to_string()
} else {
format!("sip:{server}")
};
rsip::Uri::try_from(s.as_str())
.map_err(|e| FlowcatError::Transport(format!("bad SIP server URI {server:?}: {e}")))
}
fn make_uri(user: &str, host_with_port: rsip::HostWithPort) -> rsip::Uri {
rsip::Uri {
scheme: Some(rsip::Scheme::Sip),
auth: Some(rsip::Auth {
user: user.to_string(),
password: None,
}),
host_with_port,
params: vec![],
headers: vec![],
}
}
fn uri_user(uri: Option<rsip::Uri>) -> String {
uri.and_then(|u| u.auth.map(|a| a.user)).unwrap_or_default()
}
fn reregister_delay_secs(expires: u32) -> u64 {
(expires as u64).max(MIN_REREGISTER_SECS) * 3 / 4
}
fn local_advertise_ip(endpoint: &rsipstack::transaction::endpoint::Endpoint) -> Ipv4Addr {
endpoint
.get_addrs()
.first()
.and_then(|a| SocketAddr::try_from(a.addr.clone()).ok())
.and_then(|sa| match sa.ip() {
IpAddr::V4(v4) => Some(v4),
IpAddr::V6(_) => None,
})
.unwrap_or(Ipv4Addr::LOCALHOST)
}
async fn bind_rtp_socket(base: u16, tries: u16) -> Result<(UdpSocket, u16), FlowcatError> {
let base = base & !1;
for i in 0..tries {
let Some(port) = i.checked_mul(2).and_then(|off| base.checked_add(off)) else {
break;
};
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port);
if let Ok(sock) = UdpSocket::bind(addr).await {
return Ok((sock, port));
}
}
Err(FlowcatError::Transport(format!(
"no free RTP port in {}..{}",
base,
base.saturating_add(tries.saturating_mul(2))
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_server_uri_adds_scheme() {
let u = parse_server_uri("sip.zadarma.com").unwrap();
assert_eq!(u.scheme, Some(rsip::Scheme::Sip));
assert!(u.to_string().contains("sip.zadarma.com"));
}
#[test]
fn parse_server_uri_keeps_explicit_scheme() {
let u = parse_server_uri("sip:1.2.3.4:5070").unwrap();
assert!(u.to_string().contains("1.2.3.4"));
}
#[test]
fn make_uri_sets_user_and_scheme() {
let host = rsip::HostWithPort::try_from("sip.example.com:5060").unwrap();
let u = make_uri("+15551234567", host);
assert_eq!(u.auth.as_ref().unwrap().user, "+15551234567");
assert_eq!(u.scheme, Some(rsip::Scheme::Sip));
}
#[test]
fn uri_user_extracts_number_or_empty() {
let host = rsip::HostWithPort::try_from("h:5060").unwrap();
let u = make_uri("18005551212", host);
assert_eq!(uri_user(Some(u)), "18005551212");
assert_eq!(uri_user(None), "");
}
fn z_host() -> rsip::HostWithPort {
rsip::HostWithPort::try_from("pbx.zadarma.com:5060").unwrap()
}
#[test]
fn uri_user_preserves_zadarma_did_verbatim() {
assert_eq!(
uri_user(Some(make_uri("12025550100", z_host()))),
"12025550100"
);
}
#[test]
fn uri_user_keeps_leading_plus() {
assert_eq!(
uri_user(Some(make_uri("+12025550100", z_host()))),
"+12025550100"
);
}
#[test]
fn uri_user_extension_and_junk_pass_through_for_fail_closed_404() {
assert_eq!(uri_user(Some(make_uri("100", z_host()))), "100");
assert_eq!(
uri_user(Some(make_uri("nmap-probe", z_host()))),
"nmap-probe"
);
}
#[test]
fn uri_user_bare_host_uri_yields_empty() {
let uri = rsip::Uri::try_from("sip:pbx.zadarma.com").unwrap();
assert_eq!(uri_user(Some(uri)), "");
}
#[test]
fn reregister_delay_uses_three_quarters_of_expiry() {
assert_eq!(reregister_delay_secs(50), 37);
}
#[test]
fn reregister_delay_floors_short_expiry() {
assert_eq!(reregister_delay_secs(0), 30 * 3 / 4);
assert_eq!(reregister_delay_secs(10), 30 * 3 / 4);
}
#[test]
fn reregister_delay_scales_long_expiry_without_overflow() {
assert_eq!(reregister_delay_secs(3600), 2700);
assert_eq!(reregister_delay_secs(u32::MAX), (u32::MAX as u64) * 3 / 4);
}
#[tokio::test]
async fn bind_rtp_socket_returns_distinct_ports() {
let (s1, p1) = bind_rtp_socket(DEFAULT_RTP_PORT_BASE, DEFAULT_RTP_PORT_TRIES)
.await
.unwrap();
let (s2, p2) = bind_rtp_socket(DEFAULT_RTP_PORT_BASE, DEFAULT_RTP_PORT_TRIES)
.await
.unwrap();
assert_ne!(p1, p2);
assert_eq!(p1 % 2, 0);
assert_eq!(p2 % 2, 0);
drop((s1, s2));
}
#[tokio::test]
async fn bind_rtp_socket_honors_custom_range() {
let base = 31000u16;
let tries = 4u16;
let (s, p) = bind_rtp_socket(base, tries).await.unwrap();
assert!(p >= base && p < base + tries * 2, "port {p} out of range");
assert_eq!(p % 2, 0);
drop(s);
}
#[tokio::test]
async fn bind_rtp_socket_yields_even_port_even_for_odd_base() {
let (s, p) = bind_rtp_socket(31001, 8).await.unwrap(); assert_eq!(
p % 2,
0,
"RTP port {p} is odd despite odd base (RFC 3550 wants even)"
);
drop(s);
}
}