use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use bytes::Bytes;
use turn::auth::{AuthHandler, generate_auth_key};
use turn::relay::relay_static::RelayAddressGeneratorStatic;
use turn::server::Server;
use turn::server::config::{ConnConfig, ServerConfig};
use webrtc::api::APIBuilder;
use webrtc::data_channel::RTCDataChannel;
use webrtc::data_channel::data_channel_message::DataChannelMessage;
use webrtc::ice_transport::ice_server::RTCIceServer;
use webrtc::peer_connection::RTCPeerConnection;
use webrtc::peer_connection::configuration::RTCConfiguration;
use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState;
use webrtc::peer_connection::policy::ice_transport_policy::RTCIceTransportPolicy;
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
const OPEN_TIMEOUT: Duration = Duration::from_secs(20);
const MAX_SCTP_MESSAGE: usize = 16 * 1024;
fn rt() -> &'static tokio::runtime::Runtime {
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("build webrtc tokio runtime")
})
}
const DEFAULT_STUN: &str = "stun:stun.l.google.com:19302";
#[derive(Clone, Default, Debug)]
pub struct IceServer {
pub urls: Vec<String>,
pub username: String,
pub credential: String,
}
impl IceServer {
pub fn stun(url: impl Into<String>) -> Self {
Self {
urls: vec![url.into()],
..Default::default()
}
}
pub fn turn(
url: impl Into<String>,
username: impl Into<String>,
credential: impl Into<String>,
) -> Self {
Self {
urls: vec![url.into()],
username: username.into(),
credential: credential.into(),
}
}
}
#[derive(Clone, Default, Debug)]
pub struct WebrtcConfig {
pub ice_servers: Vec<IceServer>,
pub relay_only: bool,
}
impl WebrtcConfig {
pub fn hermetic() -> Self {
Self::default()
}
pub fn from_env() -> Self {
let mut ice_servers = vec![IceServer::stun(DEFAULT_STUN)];
if let Ok(url) = std::env::var("OSFKB_WEBRTC_TURN_URL")
&& !url.is_empty()
{
ice_servers.push(IceServer::turn(
url,
std::env::var("OSFKB_WEBRTC_TURN_USER").unwrap_or_default(),
std::env::var("OSFKB_WEBRTC_TURN_PASS").unwrap_or_default(),
));
}
Self {
ice_servers,
relay_only: std::env::var("OSFKB_WEBRTC_RELAY_ONLY")
.is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true")),
}
}
}
fn configuration(cfg: &WebrtcConfig) -> RTCConfiguration {
RTCConfiguration {
ice_servers: cfg
.ice_servers
.iter()
.map(|s| RTCIceServer {
urls: s.urls.clone(),
username: s.username.clone(),
credential: s.credential.clone(),
})
.collect(),
ice_transport_policy: if cfg.relay_only {
RTCIceTransportPolicy::Relay
} else {
RTCIceTransportPolicy::default()
},
..Default::default()
}
}
type ByteTxSlot = Arc<Mutex<Option<Sender<Vec<u8>>>>>;
fn ensure_crypto_provider() {
static INSTALL: std::sync::Once = std::sync::Once::new();
INSTALL.call_once(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});
}
async fn new_peer_connection(
cfg: &WebrtcConfig,
tx_slot: ByteTxSlot,
) -> Result<Arc<RTCPeerConnection>, String> {
ensure_crypto_provider();
let api = APIBuilder::new().build();
let pc = api
.new_peer_connection(configuration(cfg))
.await
.map_err(|e| format!("new peer connection: {e}"))?;
pc.on_peer_connection_state_change(Box::new(move |state| {
let tx_slot = Arc::clone(&tx_slot);
Box::pin(async move {
eprintln!("webrtc: peer connection state {state}");
if matches!(
state,
RTCPeerConnectionState::Disconnected
| RTCPeerConnectionState::Failed
| RTCPeerConnectionState::Closed
) {
*tx_slot.lock().expect("byte tx slot poisoned") = None;
}
})
}));
if std::env::var_os("OSFKB_WEBRTC_ICE_DEBUG").is_some() {
pc.on_ice_candidate(Box::new(|cand| {
Box::pin(async move {
if let Some(c) = cand
&& let Ok(init) = c.to_json()
{
eprintln!("webrtc ICE local candidate: {}", init.candidate);
}
})
}));
pc.on_ice_connection_state_change(Box::new(|state| {
Box::pin(async move { eprintln!("webrtc ICE connection state: {state}") })
}));
}
Ok(Arc::new(pc))
}
fn wire_inbound(dc: &RTCDataChannel, tx_slot: ByteTxSlot) {
dc.on_message(Box::new(move |msg: DataChannelMessage| {
let tx_slot = Arc::clone(&tx_slot);
Box::pin(async move {
if let Some(tx) = tx_slot.lock().expect("byte tx slot poisoned").as_ref() {
let _ = tx.send(msg.data.to_vec());
}
})
}));
}
pub struct NativeWebrtcPeer {
pc: Arc<RTCPeerConnection>,
dc: Option<Arc<RTCDataChannel>>,
incoming: Option<tokio::sync::mpsc::UnboundedReceiver<Arc<RTCDataChannel>>>,
bytes_rx: Option<Receiver<Vec<u8>>>,
open_rx: Option<tokio::sync::mpsc::UnboundedReceiver<()>>,
}
impl NativeWebrtcPeer {
pub async fn make_offer(cfg: &WebrtcConfig) -> Result<(Self, String), String> {
let (bytes_tx, bytes_rx) = std::sync::mpsc::channel();
let tx_slot: ByteTxSlot = Arc::new(Mutex::new(Some(bytes_tx)));
let pc = new_peer_connection(cfg, Arc::clone(&tx_slot)).await?;
let (open_tx, open_rx) = tokio::sync::mpsc::unbounded_channel();
let dc = pc
.create_data_channel("lfm2", None)
.await
.map_err(|e| format!("create_data_channel: {e}"))?;
wire_inbound(&dc, tx_slot);
dc.on_open(Box::new(move || {
Box::pin(async move {
let _ = open_tx.send(());
})
}));
let offer = pc
.create_offer(None)
.await
.map_err(|e| format!("create_offer: {e}"))?;
let mut gather = pc.gathering_complete_promise().await;
pc.set_local_description(offer)
.await
.map_err(|e| format!("set_local(offer): {e}"))?;
let _ = gather.recv().await;
let sdp = pc
.local_description()
.await
.ok_or("no local description after gathering")?
.sdp;
Ok((
Self {
pc,
dc: Some(dc),
incoming: None,
bytes_rx: Some(bytes_rx),
open_rx: Some(open_rx),
},
sdp,
))
}
pub async fn accept_offer(
offer_sdp: &str,
cfg: &WebrtcConfig,
) -> Result<(Self, String), String> {
let (bytes_tx, bytes_rx) = std::sync::mpsc::channel();
let tx_slot: ByteTxSlot = Arc::new(Mutex::new(Some(bytes_tx)));
let pc = new_peer_connection(cfg, Arc::clone(&tx_slot)).await?;
let (dc_tx, dc_rx) = tokio::sync::mpsc::unbounded_channel();
pc.on_data_channel(Box::new(move |dc: Arc<RTCDataChannel>| {
let tx_slot = Arc::clone(&tx_slot);
let dc_tx = dc_tx.clone();
Box::pin(async move {
eprintln!("webrtc data channel received (answerer)");
wire_inbound(&dc, tx_slot);
let _ = dc_tx.send(dc);
})
}));
pc.set_remote_description(
RTCSessionDescription::offer(offer_sdp.to_owned())
.map_err(|e| format!("parse offer: {e}"))?,
)
.await
.map_err(|e| format!("set_remote(offer): {e}"))?;
let answer = pc
.create_answer(None)
.await
.map_err(|e| format!("create_answer: {e}"))?;
let mut gather = pc.gathering_complete_promise().await;
pc.set_local_description(answer)
.await
.map_err(|e| format!("set_local(answer): {e}"))?;
let _ = gather.recv().await;
let sdp = pc
.local_description()
.await
.ok_or("no local description after gathering")?
.sdp;
Ok((
Self {
pc,
dc: None,
incoming: Some(dc_rx),
bytes_rx: Some(bytes_rx),
open_rx: None,
},
sdp,
))
}
pub async fn accept_answer(&self, answer_sdp: &str) -> Result<(), String> {
self.pc
.set_remote_description(
RTCSessionDescription::answer(answer_sdp.to_owned())
.map_err(|e| format!("parse answer: {e}"))?,
)
.await
.map_err(|e| format!("set_remote(answer): {e}"))?;
Ok(())
}
pub async fn open_pipe(mut self) -> Result<WebrtcPipe, String> {
let dc = match self.dc.take() {
Some(dc) => {
let mut open_rx = self.open_rx.take().ok_or("offerer missing open signal")?;
match tokio::time::timeout(OPEN_TIMEOUT, open_rx.recv()).await {
Ok(Some(())) => dc,
_ => return Err("webrtc channel did not open in time".into()),
}
}
None => {
let incoming = self.incoming.as_mut().ok_or("answerer missing incoming")?;
match tokio::time::timeout(OPEN_TIMEOUT, incoming.recv()).await {
Ok(Some(dc)) => dc,
_ => return Err("inbound data channel never arrived".into()),
}
}
};
let bytes_rx = self.bytes_rx.take().ok_or("pipe already taken")?;
Ok(WebrtcPipe {
_pc: self.pc,
dc,
bytes_rx,
buf: Vec::new(),
pos: 0,
})
}
}
pub struct WebrtcPipe {
_pc: Arc<RTCPeerConnection>,
dc: Arc<RTCDataChannel>,
bytes_rx: Receiver<Vec<u8>>,
buf: Vec<u8>,
pos: usize,
}
impl std::io::Read for WebrtcPipe {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
while self.pos >= self.buf.len() {
match self.bytes_rx.recv() {
Ok(b) => {
self.buf = b;
self.pos = 0;
}
Err(_) => return Ok(0),
}
}
let n = out.len().min(self.buf.len() - self.pos);
out[..n].copy_from_slice(&self.buf[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
impl std::io::Write for WebrtcPipe {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
for chunk in buf.chunks(MAX_SCTP_MESSAGE) {
let data = Bytes::copy_from_slice(chunk);
rt().block_on(self.dc.send(&data))
.map_err(std::io::Error::other)?;
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub fn loopback_pipes(cfg: &WebrtcConfig) -> Result<(WebrtcPipe, WebrtcPipe), String> {
rt().block_on(async {
let (offerer, offer) = NativeWebrtcPeer::make_offer(cfg).await?;
let (answerer, answer) = NativeWebrtcPeer::accept_offer(&offer, cfg).await?;
offerer.accept_answer(&answer).await?;
let (op, ap) = tokio::join!(offerer.open_pipe(), answerer.open_pipe());
Ok((op?, ap?))
})
}
struct StaticTurnAuth {
user: String,
key: Vec<u8>,
}
impl AuthHandler for StaticTurnAuth {
fn auth_handle(
&self,
username: &str,
_realm: &str,
_src_addr: std::net::SocketAddr,
) -> Result<Vec<u8>, turn::Error> {
if username == self.user {
Ok(self.key.clone())
} else {
Err(turn::Error::ErrFakeErr)
}
}
}
pub struct LocalTurnServer {
server: Server,
pub url: String,
pub username: String,
pub credential: String,
}
impl LocalTurnServer {
pub fn ice_server(&self) -> IceServer {
IceServer::turn(&self.url, &self.username, &self.credential)
}
pub fn close(self) {
let _ = rt().block_on(self.server.close());
}
}
pub fn spawn_local_turn_server() -> Result<LocalTurnServer, String> {
rt().block_on(async {
let (user, pass, realm) = ("lfm2", "lfm2-turn-pass", "lfm2");
let socket = Arc::new(
tokio::net::UdpSocket::bind("127.0.0.1:0")
.await
.map_err(|e| format!("turn bind: {e}"))?,
);
let port = socket
.local_addr()
.map_err(|e| format!("turn addr: {e}"))?
.port();
let key = generate_auth_key(user, realm, pass);
let server = Server::new(ServerConfig {
conn_configs: vec![ConnConfig {
conn: socket,
relay_addr_generator: Box::new(RelayAddressGeneratorStatic {
relay_address: std::net::IpAddr::from([127, 0, 0, 1]),
address: "0.0.0.0".to_owned(),
net: Arc::new(webrtc_util::vnet::net::Net::new(None)),
}),
}],
realm: realm.to_owned(),
auth_handler: Arc::new(StaticTurnAuth {
user: user.to_owned(),
key,
}),
channel_bind_timeout: Duration::from_secs(0),
alloc_close_notify: None,
})
.await
.map_err(|e| format!("turn server: {e}"))?;
Ok(LocalTurnServer {
server,
url: format!("turn:127.0.0.1:{port}"),
username: user.to_owned(),
credential: pass.to_owned(),
})
})
}
pub fn do_offer(peer: &mut Option<NativeWebrtcPeer>) -> (u32, Vec<u8>) {
match rt().block_on(NativeWebrtcPeer::make_offer(&WebrtcConfig::from_env())) {
Ok((p, sdp)) => {
*peer = Some(p);
eprintln!("webrtc: made offer");
(0, sdp.into_bytes())
}
Err(e) => (1, e.into_bytes()),
}
}
pub fn do_answer(peer: &mut Option<NativeWebrtcPeer>, offer: &[u8]) -> (u32, Vec<u8>) {
let offer = String::from_utf8_lossy(offer).into_owned();
match rt().block_on(NativeWebrtcPeer::accept_offer(
&offer,
&WebrtcConfig::from_env(),
)) {
Ok((p, sdp)) => {
*peer = Some(p);
eprintln!("webrtc: made answer");
(0, sdp.into_bytes())
}
Err(e) => (1, e.into_bytes()),
}
}
pub fn do_finish(peer: &Option<NativeWebrtcPeer>, answer: &[u8]) -> (u32, Vec<u8>) {
let answer = String::from_utf8_lossy(answer).into_owned();
match peer.as_ref() {
Some(p) => match rt().block_on(p.accept_answer(&answer)) {
Ok(()) => {
eprintln!("webrtc: applied answer — channel establishing");
(0, Vec::new())
}
Err(e) => (1, e.into_bytes()),
},
None => (1, b"no pending webrtc peer for finish".to_vec()),
}
}
pub fn open_pipe_blocking(peer: Option<NativeWebrtcPeer>) -> Result<WebrtcPipe, String> {
let peer = peer.ok_or("no pending webrtc peer to open")?;
rt().block_on(peer.open_pipe())
}