#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::cast_possible_truncation
)]
mod interop_media;
use std::time::Duration;
use bytes::Bytes;
#[cfg(feature = "dtls")]
use sipx_call::Keying;
use sipx_call::{DialOptions, dial};
use sipx_sip::Uri;
use sipx_transport::{Config as TransportConfig, Target, TransportKind, bind};
use interop_media::{addr_in, assert_echo, echo_round_trip, loopback};
fn sdes_uri() -> String {
std::env::var("SIPX_INTEROP_SDES_URI").unwrap_or_else(|_| "sip:echo@127.0.0.1:5061".to_owned())
}
fn sdes_from() -> String {
std::env::var("SIPX_INTEROP_SDES_FROM")
.unwrap_or_else(|_| "<sip:sipx-srtp@127.0.0.1>".to_owned())
}
#[cfg(feature = "dtls")]
fn dtls_uri() -> String {
std::env::var("SIPX_INTEROP_DTLS_URI").unwrap_or_else(|_| "sip:echo@127.0.0.1:5060".to_owned())
}
#[cfg(feature = "dtls")]
fn dtls_from() -> String {
std::env::var("SIPX_INTEROP_DTLS_FROM")
.unwrap_or_else(|_| "<sip:sipx-dtls@127.0.0.1>".to_owned())
}
fn rejection_marker() -> String {
std::env::var("SIPX_INTEROP_SRTP_REJECTED")
.unwrap_or_else(|_| "SRTP unprotect failed".to_owned())
}
fn media_report_command() -> String {
std::env::var("SIPX_INTEROP_MEDIA_REPORT")
.unwrap_or_else(|_| "asterisk -rx 'pjsip show channelstats'".to_owned())
}
fn interop_anchors() -> sipx_transport::tls::TrustAnchors {
let path = std::env::var("SIPX_INTEROP_CA").unwrap_or_else(|_| {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/interop/asterisk/tls/ca.pem"
)
.to_owned()
});
let pem = std::fs::read(&path)
.unwrap_or_else(|e| panic!("{path}: {e}; run ./tests/interop/run.sh, which issues it"));
let mut anchors = sipx_transport::tls::TrustAnchors::only();
anchors.add_pem(&pem).expect("a usable fixture CA");
anchors
}
fn container() -> String {
std::env::var("SIPX_INTEROP_CONTAINER").unwrap_or_else(|_| {
panic!("SIPX_INTEROP_CONTAINER is unset; run this through tests/interop/run.sh")
})
}
#[derive(Default)]
struct PeerLog {
out: String,
err: String,
}
impl PeerLog {
fn read() -> Self {
let output = std::process::Command::new("docker")
.args(["logs", &container()])
.output()
.expect("docker logs runs");
Self {
out: String::from_utf8_lossy(&output.stdout).into_owned(),
err: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}
fn since(&self, mark: &Self) -> String {
let tail = |whole: &str, seen: usize| {
whole
.get(seen.min(whole.len())..)
.unwrap_or_default()
.to_owned()
};
format!(
"{}{}",
tail(&self.out, mark.out.len()),
tail(&self.err, mark.err.len())
)
}
}
fn peer_media_report() -> String {
let output = std::process::Command::new("docker")
.args(["exec", &container(), "sh", "-c", &media_report_command()])
.output()
.expect("docker exec runs");
String::from_utf8_lossy(&output.stdout).into_owned()
}
#[tokio::test]
#[ignore = "needs a user agent peer that keys media with SDES; see tests/interop/README.md"]
async fn a_real_peer_accepts_media_sipx_encrypted_with_sdes() {
let said_before = PeerLog::read();
let mut config = TransportConfig::new("127.0.0.1:0".parse().expect("valid"));
config.sent_by = loopback().to_string();
config.tls_client =
Some(sipx_transport::tls::ClientTls::new(&interop_anchors()).expect("a client"));
let (handle, _incoming) = bind(config).await.expect("binds");
let uri = sdes_uri();
let to = Uri::parse(Bytes::from(uri.clone())).expect("a SIP URI");
let target = Target::new(addr_in(&uri), TransportKind::Tls).verifying("sipx.test");
let options = DialOptions::new(sdes_from(), loopback()).with_timeout(Duration::from_secs(15));
let mut call = tokio::time::timeout(
Duration::from_secs(20),
dial(&handle, target, &to, &options),
)
.await
.expect("the peer answers rather than leaving us ringing")
.expect(
"the peer accepts the encrypted call; it is configured to require SRTP, so a \
plain offer is refused rather than answered",
);
assert!(
call.is_encrypted(),
"the call connected with cleartext media; `RTP/SAVP` was offered and the answer did not \
key it, so nothing here would have exercised SRTP at all"
);
let codec = call.media().codec();
assert_eq!(
codec.payload_type(),
0,
"the negotiation chose {codec:?}; the offer's first and the peer's configured codec is µ-law"
);
let (sent, echoed) = echo_round_trip(&call).await;
let report = peer_media_report();
let since = PeerLog::read().since(&said_before);
let rejected = rejection_marker();
let complaints: Vec<&str> = since
.lines()
.filter(|line| line.contains(rejected.as_str()))
.collect();
assert!(
complaints.is_empty(),
"the peer refused sipx's encrypted packets — its words, not ours:\n {}\n\nand its \
account of the media:\n{report}",
complaints.join("\n ")
);
assert_echo(&sent, &echoed, codec.payload_type());
call.hang_up().await.expect("the BYE is accepted");
assert!(call.is_ended(), "the call is over on our side too");
}
#[cfg(feature = "dtls")]
#[tokio::test]
#[ignore = "needs a user agent peer that keys media with DTLS-SRTP; see tests/interop/README.md"]
async fn a_real_peer_accepts_media_sipx_encrypted_with_dtls_srtp() {
let said_before = PeerLog::read();
let (handle, _incoming) = bind(TransportConfig::new("127.0.0.1:0".parse().expect("valid")))
.await
.expect("binds");
let uri = dtls_uri();
let to = Uri::parse(Bytes::from(uri.clone())).expect("a SIP URI");
let target = Target::udp(addr_in(&uri));
let options = DialOptions::new(dtls_from(), loopback())
.with_keying(Keying::DtlsSrtp)
.with_timeout(Duration::from_secs(15));
let mut call = tokio::time::timeout(
Duration::from_secs(20),
dial(&handle, target, &to, &options),
)
.await
.expect("the peer answers within the call bound")
.expect("the strict DTLS-SRTP endpoint accepts the call");
assert!(
call.is_encrypted(),
"the DTLS-selected call fell back to cleartext"
);
let codec = call.media().codec();
let (sent, echoed) = echo_round_trip(&call).await;
let report = peer_media_report();
let since = PeerLog::read().since(&said_before);
let rejected = rejection_marker();
let complaints: Vec<&str> = since
.lines()
.filter(|line| line.contains(rejected.as_str()))
.collect();
assert!(
complaints.is_empty(),
"the peer refused DTLS-keyed packets:\n {}\n\npeer media report:\n{report}",
complaints.join("\n ")
);
assert_echo(&sent, &echoed, codec.payload_type());
call.hang_up().await.expect("the BYE is accepted");
assert!(call.is_ended());
}