This commit is contained in:
2026-07-31 20:20:48 +01:00
parent d4b77bd717
commit c1435a70fb
12 changed files with 1009 additions and 168 deletions
+45 -29
View File
@@ -7,7 +7,6 @@ use std::{
use bytes::Bytes;
use dashmap::DashMap;
use str0m::net::DatagramRecv;
use tokio::{
net::UdpSocket,
sync::mpsc::{self, Receiver},
@@ -24,6 +23,10 @@ pub struct WebrtcProxy {
clients_addr: Arc<DashMap<SocketAddr, tokio::sync::mpsc::Sender<(Bytes, SocketAddr)>>>,
socket: Arc<UdpSocket>,
public_addr: SocketAddr,
/// Trickle-ICE candidate channels for WHIP ingest.
/// Keyed by stream_key_id; sender stored here so PATCH handler
/// can forward candidates to the detach task.
pub trickle_tx: Arc<DashMap<i32, tokio::sync::mpsc::UnboundedSender<String>>>,
}
const STUN_MAGIC: u32 = 0x2112A442;
@@ -61,6 +64,7 @@ impl WebrtcProxy {
clients_ufrag: Arc::new(DashMap::new()),
clients_addr: Arc::new(DashMap::new()),
public_addr,
trickle_tx: Arc::new(DashMap::new()),
})
}
pub async fn run(self) {
@@ -79,8 +83,9 @@ impl WebrtcProxy {
};
let data = Bytes::copy_from_slice(&buf[..b]);
// By addr
if let Some(tx) = by_addr.get(&from) {
// By addr
if let Some(tx) = by_addr.get(&from) {
debug!("proxy: routing {} bytes by addr {}:{} → channel", b, from.ip(), from.port());
match tx.try_send((data, from)) {
Ok(_) => continue,
Err(e) => {
@@ -97,18 +102,24 @@ impl WebrtcProxy {
};
};
let Some(ufrag) = self::WebrtcProxy::ufrag(&data) else {
let Some((part1, part2)) = self::WebrtcProxy::ufrag_pair(&data) else {
debug!("huh, packet isnt stun or added as client.");
continue;
};
let Some((_, tx)) = by_ufrag.remove(&ufrag) else {
// warn!("STUN packet ({}), isnt registored", ufrag);
// Try both parts of the STUN username — the first packet
// might be a response to OUR STUN request (remote:local)
// or an incoming request from the remote peer (local:remote).
let part2_lookup = part2.clone();
let entry = by_ufrag.remove(&part1).or_else(|| {
part2_lookup.and_then(|p2| by_ufrag.remove(&p2))
});
let Some((_, tx)) = entry else {
warn!("STUN packet ({}/{:?}), isnt registored", part1, part2);
continue;
};
by_addr.insert(from, tx.clone());
debug!("got ufrag {}", ufrag);
info!("proxy: STUN match → promoted {} → ufrag={} (match was {}/{})", from, part1, part1, part2.as_deref().unwrap_or("-"));
debug!("sending data");
if let Err(e) = tx.try_send((data, from)) {
match e {
@@ -136,29 +147,34 @@ impl WebrtcProxy {
pub fn public_addr(&self) -> SocketAddr {
self.public_addr
}
pub fn ufrag(b: &Bytes) -> Option<String> {
if b.len() <= 20 {
return None;
}
let magic = u32::from_be_bytes(b[4..8].try_into().ok()?);
if magic != STUN_MAGIC {
return None;
}
// attribies start at 20
let mut pos = 20usize;
while (pos + 4) <= b.len() {
let attr_type: u16 = u16::from_be_bytes(b[pos..pos + 2].try_into().ok()?);
let attr_len: u16 = u16::from_be_bytes(b[pos + 2..pos + 4].try_into().ok()?);
pos = pos + 4;
if attr_type == 0x0006 {
let value = std::str::from_utf8(b[pos..pos + (attr_len as usize)].try_into().ok()?);
let local = value.unwrap().split(":").next();
return Some(local.unwrap().to_string());
pub fn ufrag_pair(b: &Bytes) -> Option<(String, Option<String>)> {
if b.len() <= 20 {
return None;
}
pos += (attr_len as usize + 3) & !3;
let magic = u32::from_be_bytes(b[4..8].try_into().ok()?);
if magic != STUN_MAGIC {
return None;
}
// attribies start at 20
let mut pos = 20usize;
while (pos + 4) <= b.len() {
let attr_type: u16 = u16::from_be_bytes(b[pos..pos + 2].try_into().ok()?);
let attr_len: u16 = u16::from_be_bytes(b[pos + 2..pos + 4].try_into().ok()?);
pos += 4;
if attr_type == 0x0006 {
let value = std::str::from_utf8(
b[pos..pos + (attr_len as usize)].try_into().ok()?,
)
.ok()?;
let mut parts = value.split(':');
let first = parts.next()?.to_string();
let second = parts.next().map(|s| s.to_string());
return Some((first, second));
}
pos += (attr_len as usize + 3) & !3;
}
None
}
None
}
}
async fn resolve_domain(domain: &str) -> Result<std::net::IpAddr, Box<dyn Error>> {