Files
oxspeak_server/src/rtc/stun_diagnostic.rs
T
2026-09-24 13:55:59 +02:00

65 lines
3.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use tokio::net::{UdpSocket, lookup_host};
use tokio::time::timeout;
pub(super) async fn probe(url: &str, bind_ip: Ipv4Addr) -> anyhow::Result<SocketAddr> {
let endpoint = url.strip_prefix("stun:").ok_or_else(|| anyhow::anyhow!("URL STUN non prise en charge"))?;
let endpoint = endpoint.split('?').next().unwrap_or(endpoint);
let mut addresses = lookup_host(endpoint).await?;
let server = addresses.find(SocketAddr::is_ipv4).ok_or_else(|| anyhow::anyhow!("aucune adresse IPv4 pour le serveur STUN"))?;
let socket = UdpSocket::bind((bind_ip, 0)).await?;
let local = socket.local_addr()?;
let transaction = *uuid::Uuid::new_v4().as_bytes();
let mut request = [0u8; 20];
request[..2].copy_from_slice(&0x0001u16.to_be_bytes());
request[4..8].copy_from_slice(&0x2112a442u32.to_be_bytes());
request[8..20].copy_from_slice(&transaction[..12]);
tracing::info!(%server, %local, "Diagnostic STUN IPv4 : requête Binding envoyée (socket distinct d’ICE)");
socket.send_to(&request, server).await?;
let mut response = [0u8; 1500];
let (length, sender) = timeout(Duration::from_secs(5), socket.recv_from(&mut response)).await??;
anyhow::ensure!(sender == server, "réponse STUN d'une autre adresse : {sender}");
let mapped = mapped_address(&response[..length], &transaction[..12])?;
tracing::info!(%server, %local, %mapped, "Diagnostic STUN IPv4 : réponse Binding reçue");
Ok(mapped)
}
fn mapped_address(response: &[u8], transaction: &[u8]) -> anyhow::Result<SocketAddr> {
anyhow::ensure!(response.len() >= 20 && response[0..2] == [0x01, 0x01]
&& response[4..8] == 0x2112a442u32.to_be_bytes()
&& response[8..20] == *transaction, "réponse STUN invalide ou transaction différente");
let size = u16::from_be_bytes([response[2], response[3]]) as usize;
anyhow::ensure!(response.len() >= 20 + size, "réponse STUN tronquée");
let mut offset = 20;
while offset + 4 <= 20 + size {
let kind = u16::from_be_bytes([response[offset], response[offset + 1]]);
let len = u16::from_be_bytes([response[offset + 2], response[offset + 3]]) as usize;
offset += 4;
anyhow::ensure!(offset + len <= 20 + size, "attribut STUN tronqué");
if kind == 0x0020 && len >= 8 && response[offset + 1] == 1 {
let port = u16::from_be_bytes([response[offset + 2], response[offset + 3]]) ^ 0x2112;
let cookie = 0x2112a442u32.to_be_bytes();
let ip = Ipv4Addr::from(std::array::from_fn(|i| response[offset + 4 + i] ^ cookie[i]));
return Ok(SocketAddr::new(IpAddr::V4(ip), port));
}
offset += (len + 3) & !3;
}
anyhow::bail!("aucune adresse IPv4 XOR-MAPPED-ADDRESS dans la réponse STUN")
}
#[cfg(test)]
mod tests {
use super::mapped_address;
#[test]
fn decodes_ipv4_and_rejects_unrelated_response() {
let transaction = [7; 12];
let mut response = vec![0x01, 0x01, 0, 12, 0x21, 0x12, 0xa4, 0x42];
response.extend(transaction);
response.extend([0, 0x20, 0, 8, 0, 1, 0x3f, 0x52, 0x6a, 0x22, 0xa5, 0x43]);
assert_eq!(mapped_address(&response, &transaction).unwrap().to_string(), "75.48.1.1:7744");
assert!(mapped_address(&response, &[8; 12]).is_err());
assert!(mapped_address(&response[..25], &transaction).is_err());
}
}