mod crypt;
mod link;
mod packet;
use std::io;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use bytes::Bytes;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
pub use crypt::CryptStats;
pub use link::{LinkStats, Transport};
use crate::client::Handle;
use crate::error::CommandError;
use crate::state::CryptSetup;
use crypt::{CryptState, OVERHEAD};
use link::{Link, PING_INTERVAL};
use packet::{AudioIn, AudioOut, Format, Incoming, MAX_PACKET};
pub use packet::MAX_VOICE_PAYLOAD;
const OVERSIZE_LOG_INTERVAL: Duration = Duration::from_secs(10);
const CONTEXT_LISTEN: u8 = 3;
#[derive(Debug, Clone, PartialEq)]
pub struct OutgoingAudio {
pub payload: Bytes,
pub frames: u8,
pub is_last: bool,
pub target: u8,
pub positional: Option<[f32; 3]>,
}
impl OutgoingAudio {
pub fn new(payload: Bytes, frames: u8, is_last: bool) -> Self {
Self {
payload,
frames,
is_last,
target: 0,
positional: None,
}
}
pub fn with_position(mut self, positional: Option<[f32; 3]>) -> Self {
self.positional = positional;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VoicePacket {
pub session: u32,
pub seq: u64,
pub payload: Bytes,
pub is_last: bool,
pub via_listener: bool,
pub positional: Option<[f32; 3]>,
pub transport: Transport,
}
#[derive(Debug, Clone, PartialEq)]
pub enum VoiceEvent {
Packet(VoicePacket),
TransportChanged(Transport),
}
enum Command {
Audio(OutgoingAudio),
CryptSetup(CryptSetup),
Tunnel(Bytes),
}
pub struct VoiceTransport {
tx: mpsc::UnboundedSender<Command>,
stats: Arc<Mutex<LinkStats>>,
task: JoinHandle<()>,
}
impl VoiceTransport {
pub async fn start(
handle: Handle,
server: SocketAddr,
) -> io::Result<(Self, mpsc::UnboundedReceiver<VoiceEvent>)> {
let local: SocketAddr = if server.is_ipv4() {
(Ipv4Addr::UNSPECIFIED, 0).into()
} else {
(Ipv6Addr::UNSPECIFIED, 0).into()
};
let socket = UdpSocket::bind(local).await?;
socket.connect(server).await?;
let now = Instant::now();
let link = Link::new(now);
let mut crypt = CryptState::new();
let initial = handle.state().crypt;
let mut applied = None;
if let Some(setup) = initial {
apply_full_key(&mut crypt, &mut applied, &setup);
}
let stats = Arc::new(Mutex::new(link.stats(crypt.stats)));
let (tx, rx) = mpsc::unbounded_channel();
let (events_tx, events) = mpsc::unbounded_channel();
let worker = Worker {
format: Format::for_server(handle.server_version()),
handle,
socket,
crypt,
applied,
link,
started: now,
seq: 0,
oversized: 0,
oversized_logged: 0,
audio_sent: 0,
audio_received: 0,
oversize_logged_at: None,
events: events_tx,
stats: Arc::clone(&stats),
};
let task = tokio::spawn(worker.run(rx));
Ok((Self { tx, stats, task }, events))
}
pub fn send_audio(&self, audio: OutgoingAudio) {
let _ = self.tx.send(Command::Audio(audio));
}
pub fn on_crypt_setup(&self, setup: CryptSetup) {
let _ = self.tx.send(Command::CryptSetup(setup));
}
pub fn on_tunnel(&self, packet: Bytes) {
let _ = self.tx.send(Command::Tunnel(packet));
}
pub fn stats(&self) -> LinkStats {
*lock(&self.stats)
}
pub fn transport(&self) -> Transport {
lock(&self.stats).transport
}
pub fn is_running(&self) -> bool {
!self.task.is_finished()
}
}
impl Drop for VoiceTransport {
fn drop(&mut self) {
self.task.abort();
}
}
fn lock(stats: &Mutex<LinkStats>) -> MutexGuard<'_, LinkStats> {
stats.lock().unwrap_or_else(|p| p.into_inner())
}
type KeyMaterial = (Vec<u8>, Vec<u8>, Vec<u8>);
fn apply_full_key(
crypt: &mut CryptState,
applied: &mut Option<KeyMaterial>,
setup: &CryptSetup,
) -> bool {
let (Some(key), Some(cn), Some(sn)) = (&setup.key, &setup.client_nonce, &setup.server_nonce)
else {
return false;
};
let material = (key.clone(), cn.clone(), sn.clone());
if applied.as_ref() == Some(&material) {
return true;
}
if crypt.set_key(key, cn, sn) {
*applied = Some(material);
} else {
tracing::warn!("ignoring CryptSetup with invalid key or nonce length");
}
true
}
struct Worker {
handle: Handle,
socket: UdpSocket,
format: Format,
crypt: CryptState,
applied: Option<KeyMaterial>,
link: Link,
started: Instant,
seq: u64,
oversized: u64,
oversized_logged: u64,
oversize_logged_at: Option<Instant>,
audio_sent: u64,
audio_received: u64,
events: mpsc::UnboundedSender<VoiceEvent>,
stats: Arc<Mutex<LinkStats>>,
}
impl Worker {
async fn run(mut self, mut commands: mpsc::UnboundedReceiver<Command>) {
let mut ticker = tokio::time::interval(PING_INTERVAL);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut buf = vec![0u8; MAX_PACKET + OVERHEAD + 64];
loop {
let result = tokio::select! {
received = self.socket.recv(&mut buf) => {
match received {
Ok(n) => self.on_udp(&buf[..n]),
Err(err) => tracing::debug!(error = %err, "udp receive failed"),
}
Ok(())
}
command = commands.recv() => match command {
Some(Command::Audio(audio)) => self.send_audio(&audio).await,
Some(Command::CryptSetup(setup)) => self.on_crypt_setup(&setup),
Some(Command::Tunnel(packet)) => {
self.on_tunnel(&packet);
Ok(())
}
None => break,
},
_ = ticker.tick() => self.on_tick().await,
};
if result.is_err() || !self.handle.is_connected() {
break;
}
let mut stats = self.link.stats(self.crypt.stats);
stats.oversized = self.oversized;
stats.audio_sent = self.audio_sent;
stats.audio_received = self.audio_received;
*lock(&self.stats) = stats;
}
}
fn emit(&self, event: VoiceEvent) {
let _ = self.events.send(event);
}
fn on_udp(&mut self, data: &[u8]) {
let now = Instant::now();
let Some(plain) = self.crypt.decrypt(data) else {
if self.crypt.is_valid() && self.link.decrypt_failed(now) {
tracing::info!("udp decrypt keeps failing, requesting crypt resync");
if let Err(err) = self.handle.send_crypt_setup(CryptSetup::default()) {
tracing::debug!(error = %err, "crypt resync request not sent");
}
}
return;
};
self.link.udp_received(now);
match packet::decode(self.format, &plain) {
Some(Incoming::Ping(timestamp)) => {
let rtt = now
.duration_since(self.started)
.saturating_sub(std::time::Duration::from_micros(timestamp));
if let Some(to) = self.link.ping_reply(timestamp, rtt) {
tracing::info!(transport = ?to, "voice transport changed");
self.emit(VoiceEvent::TransportChanged(to));
}
}
Some(Incoming::Audio(audio)) => self.emit_audio(audio, Transport::Udp),
None => tracing::debug!("dropping undecodable udp voice packet"),
}
}
fn on_tunnel(&mut self, data: &[u8]) {
match packet::decode(self.format, data) {
Some(Incoming::Audio(audio)) => self.emit_audio(audio, Transport::Tcp),
Some(Incoming::Ping(_)) => {}
None => tracing::debug!("dropping undecodable tunnelled voice packet"),
}
}
fn emit_audio(&mut self, audio: AudioIn, transport: Transport) {
self.audio_received += 1;
self.emit(VoiceEvent::Packet(VoicePacket {
session: audio.session,
seq: audio.seq,
payload: audio.payload,
is_last: audio.is_last,
via_listener: audio.context == CONTEXT_LISTEN,
positional: audio.positional,
transport,
}));
}
fn on_crypt_setup(&mut self, setup: &CryptSetup) -> Result<(), CommandError> {
if apply_full_key(&mut self.crypt, &mut self.applied, setup) {
return Ok(());
}
if let Some(nonce) = &setup.server_nonce {
if self.crypt.set_decrypt_iv(nonce) {
self.crypt.stats.resync = self.crypt.stats.resync.wrapping_add(1);
}
return Ok(());
}
if self.crypt.is_valid() {
return self.handle.send_crypt_setup(CryptSetup {
client_nonce: Some(self.crypt.encrypt_iv()),
..CryptSetup::default()
});
}
Ok(())
}
async fn send_audio(&mut self, audio: &OutgoingAudio) -> Result<(), CommandError> {
let out = AudioOut {
target: audio.target,
seq: self.seq,
payload: &audio.payload,
is_last: audio.is_last,
positional: audio.positional,
};
self.seq += u64::from(audio.frames);
let Some(plain) = packet::encode_audio(self.format, &out).filter(|p| p.len() <= MAX_PACKET)
else {
self.oversized += 1;
self.log_oversized(audio.payload.len());
return Ok(());
};
self.audio_sent += 1;
match self.link.transport() {
Transport::Udp => {
self.send_udp(&plain).await;
Ok(())
}
Transport::Tcp => self.handle.send_tunnel(Bytes::from(plain)),
}
}
fn log_oversized(&mut self, payload_len: usize) {
let now = Instant::now();
if self
.oversize_logged_at
.is_some_and(|at| now.duration_since(at) < OVERSIZE_LOG_INTERVAL)
{
return;
}
tracing::warn!(
payload_len,
dropped = self.oversized - self.oversized_logged,
"dropping oversized voice packets; lowering the bitrate"
);
self.oversized_logged = self.oversized;
self.oversize_logged_at = Some(now);
}
async fn send_udp(&mut self, plain: &[u8]) {
let Some(packet) = self.crypt.encrypt(plain) else {
return;
};
if let Err(err) = self.socket.send(&packet).await {
tracing::debug!(error = %err, "udp send failed");
}
}
async fn on_tick(&mut self) -> Result<(), CommandError> {
let now = Instant::now();
if let Some(to) = self.link.tick(now, self.crypt.stats) {
tracing::info!(transport = ?to, "voice transport changed");
self.emit(VoiceEvent::TransportChanged(to));
}
if self.crypt.is_valid() {
let timestamp =
u64::try_from(now.duration_since(self.started).as_micros()).unwrap_or(u64::MAX);
let ping = packet::encode_ping(self.format, timestamp);
self.link.ping_sent(timestamp);
self.send_udp(&ping).await;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repeated_login_key_does_not_rewind_nonces() {
let setup = CryptSetup {
key: Some(vec![1; 16]),
client_nonce: Some(vec![2; 16]),
server_nonce: Some(vec![3; 16]),
};
let mut crypt = CryptState::new();
let mut applied = None;
assert!(apply_full_key(&mut crypt, &mut applied, &setup));
let first = crypt.encrypt(b"x").expect("key set");
assert!(apply_full_key(&mut crypt, &mut applied, &setup));
let second = crypt.encrypt(b"x").expect("key set");
assert_ne!(first[0], second[0], "nonce must keep counting");
let nonce_only = CryptSetup {
server_nonce: Some(vec![4; 16]),
..CryptSetup::default()
};
assert!(!apply_full_key(&mut crypt, &mut applied, &nonce_only));
}
}