65 lines
3.4 KiB
Rust
65 lines
3.4 KiB
Rust
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());
|
||
}
|
||
} |