Files
MetaCrate/crates/libremetaverse/src/udp_transport.rs
Chili Palmer d08b59c9a9
Some checks failed
API and SemVer surface / api-surface (push) Failing after 13m8s
Native code generation / deterministic (push) Failing after 2m8s
Concurrency and resource soak audit / soak (push) Failing after 6m39s
Imaging and meshing gate / native (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
Native Rust workspace compile / compile (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
JPEG 2000 feature / linux (push) Successful in 2m46s
Skia feature / linux (push) Successful in 31m0s
Audit public API and SemVer surface (#103)
2026-08-12 01:35:26 +00:00

1862 lines
62 KiB
Rust

//! Bounded Tokio UDP transport and the C#-compatible transport value types.
#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map.
#![allow(clippy::needless_pass_by_value)] // Owned arrays and objects preserve mapped signatures.
use crate::packets::{Packet, PacketAckPacket, PacketAckPacketPacketsBlock, PacketType};
use crate::{Error, GridClient, Helpers, Simulator};
use libremetaverse_types::compat::{Array, CancellationToken, CancellationTokenSource, Object};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::fmt;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{Receiver as AckReceiver, SyncSender as AckSender, sync_channel};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle;
use tokio::time::{Instant, MissedTickBehavior};
const DEFAULT_DECODE_BUFFER_SIZE: usize = 8 * 1024;
const THROTTLE_PERIOD: Duration = Duration::from_millis(100);
const THROTTLE_MIN_BYTES_PER_PERIOD: usize = 200;
const THROTTLE_BURST_PERIODS: usize = 4;
/// A failure at the native UDP transport boundary.
///
/// The variants deliberately carry no datagram bytes, credentials, endpoint
/// query data, or operating-system error strings.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum UdpTransportError {
InvalidConfiguration(&'static str),
InvalidBuffer,
MtuExceeded,
NotRunning,
AlreadyRunning,
Backpressure,
ReliableWindowFull,
TooManyPeers,
RuntimeUnavailable,
Cancelled,
Socket,
}
impl fmt::Display for UdpTransportError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfiguration(field) => {
write!(formatter, "invalid UDP transport setting {field}")
}
Self::InvalidBuffer => formatter.write_str("invalid UDP packet buffer"),
Self::MtuExceeded => formatter.write_str("UDP payload exceeds the protocol MTU"),
Self::NotRunning => formatter.write_str("UDP transport is not running"),
Self::AlreadyRunning => formatter.write_str("UDP transport is already running"),
Self::Backpressure => formatter.write_str("UDP transport queue is full"),
Self::ReliableWindowFull => formatter.write_str("UDP reliable-send window is full"),
Self::TooManyPeers => formatter.write_str("UDP peer limit is reached"),
Self::RuntimeUnavailable => {
formatter.write_str("UDP transport requires a current Tokio runtime")
}
Self::Cancelled => formatter.write_str("UDP transport was cancelled"),
Self::Socket => formatter.write_str("UDP socket operation failed"),
}
}
}
impl std::error::Error for UdpTransportError {}
impl From<UdpTransportError> for Error {
fn from(error: UdpTransportError) -> Self {
match error {
UdpTransportError::InvalidConfiguration(_)
| UdpTransportError::InvalidBuffer
| UdpTransportError::MtuExceeded => Self::Argument,
UdpTransportError::Cancelled => Self::Cancelled,
UdpTransportError::Socket => Self::Socket,
UdpTransportError::NotRunning
| UdpTransportError::AlreadyRunning
| UdpTransportError::Backpressure
| UdpTransportError::ReliableWindowFull
| UdpTransportError::TooManyPeers
| UdpTransportError::RuntimeUnavailable => Self::InvalidOperation,
}
}
}
/// C# `UDPPacketBuffer`, with one owned allocation per queued datagram.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UDPPacketBuffer {
pub data: Vec<u8>,
pub data_length: i32,
pub remote_end_point: SocketAddr,
}
impl UDPPacketBuffer {
pub const DEFAULT_BUFFER_SIZE: i32 = 4096;
pub fn new_with_constructor() -> Result<Self, Error> {
Self::new_with_ip_end_point_int32(
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
Self::DEFAULT_BUFFER_SIZE,
)
}
pub fn new_with_bytes_int32_ip_end_point_int32(
buffer: Vec<u8>,
buffer_size: i32,
destination: SocketAddr,
_category: i32,
) -> Result<Self, Error> {
let mut packet = Self::new_with_ip_end_point_int32(destination, buffer_size)?;
packet.copy_from_slice_with_length(&buffer, buffer_size)?;
packet.data_length = buffer_size;
Ok(packet)
}
pub fn new_with_ip_end_point(end_point: SocketAddr) -> Result<Self, Error> {
Self::new_with_ip_end_point_int32(end_point, Self::DEFAULT_BUFFER_SIZE)
}
pub fn new_with_ip_end_point_bytes(
end_point: SocketAddr,
data: Vec<u8>,
) -> Result<Self, Error> {
Ok(Self {
data,
// The C# constructor adopts the supplied array but deliberately
// leaves the public DataLength field at its zero default.
data_length: 0,
remote_end_point: end_point,
})
}
pub fn new_with_ip_end_point_int32(
end_point: SocketAddr,
buffer_size: i32,
) -> Result<Self, Error> {
let buffer_size = usize::try_from(buffer_size).map_err(|_| Error::Argument)?;
let mut data = Vec::new();
data.try_reserve_exact(buffer_size)
.map_err(|_| Error::InvalidOperation)?;
data.resize(buffer_size, 0);
Ok(Self {
data,
data_length: 0,
remote_end_point: end_point,
})
}
/// Copies a mapped CLR array into this buffer.
///
/// `Object::Bytes` represents a boxed `byte[]`; a flat array of integer
/// objects is also accepted so the mapped `System.Array` remains useful.
pub fn copy_from_with_array(&mut self, src: Array) -> Result<(), Error> {
let bytes = mapped_array_bytes(src)?;
let length = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?;
self.copy_from_slice_with_length(&bytes, length)
}
pub fn copy_from_with_array_int32(&mut self, src: Array, length: i32) -> Result<(), Error> {
let bytes = mapped_array_bytes(src)?;
self.copy_from_slice_with_length(&bytes, length)
}
pub fn copy_from_slice(&mut self, src: &[u8]) -> Result<(), Error> {
let length = i32::try_from(src.len()).map_err(|_| Error::Argument)?;
self.copy_from_slice_with_length(src, length)
}
pub fn copy_from_slice_with_length(&mut self, src: &[u8], length: i32) -> Result<(), Error> {
let length = usize::try_from(length).map_err(|_| Error::Argument)?;
if length > src.len() || length > self.data.len() {
return Err(Error::IndexOutOfRange);
}
self.data[..length].copy_from_slice(&src[..length]);
Ok(())
}
pub fn reset_endpoint(&mut self) -> Result<(), Error> {
self.remote_end_point = match self.remote_end_point.ip() {
IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
};
Ok(())
}
fn payload(&self) -> Result<&[u8], UdpTransportError> {
let length =
usize::try_from(self.data_length).map_err(|_| UdpTransportError::InvalidBuffer)?;
self.data
.get(..length)
.ok_or(UdpTransportError::InvalidBuffer)
}
}
fn mapped_array_bytes(src: Array) -> Result<Vec<u8>, Error> {
if let [Object::Bytes(bytes)] = src.0.as_slice() {
return Ok(bytes.clone());
}
let mut bytes = Vec::new();
bytes
.try_reserve_exact(src.0.len())
.map_err(|_| Error::InvalidOperation)?;
for value in src.0 {
let byte = match value {
Object::Integer(value) => u8::try_from(value).map_err(|_| Error::Argument)?,
Object::UInteger(value) => u8::try_from(value).map_err(|_| Error::Argument)?,
_ => return Err(Error::Argument),
};
bytes.push(byte);
}
Ok(bytes)
}
struct PacketArchiveState {
items: Vec<u32>,
members: HashSet<u32>,
first: usize,
next: usize,
}
/// Fixed-size duplicate archive matching `IncomingPacketIDCollection`.
pub struct IncomingPacketIDCollection {
capacity: usize,
state: Mutex<PacketArchiveState>,
}
impl IncomingPacketIDCollection {
pub fn new(capacity: i32) -> Result<Self, Error> {
let capacity = usize::try_from(capacity).map_err(|_| Error::Argument)?;
if capacity == 0 {
return Err(Error::Argument);
}
let mut items = Vec::new();
items
.try_reserve_exact(capacity)
.map_err(|_| Error::InvalidOperation)?;
items.resize(capacity, 0);
Ok(Self {
capacity,
state: Mutex::new(PacketArchiveState {
items,
members: HashSet::with_capacity(capacity),
first: 0,
next: 0,
}),
})
}
pub fn try_enqueue(&self, ack: u32) -> bool {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !state.members.insert(ack) {
return false;
}
let next = state.next;
state.items[next] = ack;
state.next = (next + 1) % self.capacity;
if state.next == state.first {
let first = state.first;
let removed = state.items[first];
state.members.remove(&removed);
state.first = (first + 1) % self.capacity;
}
true
}
}
impl fmt::Debug for IncomingPacketIDCollection {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
formatter
.debug_struct("IncomingPacketIDCollection")
.field("capacity", &self.capacity)
.field("len", &state.members.len())
.finish()
}
}
/// Injection boundary used by `AgentThrottle::Set` and the later network
/// manager composition layer.
pub trait AgentThrottleSender: Send + Sync {
fn send_throttle(
&self,
throttle_bytes: &[u8],
simulator: Option<&Simulator>,
) -> Result<(), Error>;
}
/// Exact seven-stream C# throttle values and little-endian wire encoding.
#[derive(Clone)]
pub struct AgentThrottle {
resend: f32,
land: f32,
wind: f32,
cloud: f32,
task: f32,
texture: f32,
asset: f32,
sender: Option<Arc<dyn AgentThrottleSender>>,
}
impl AgentThrottle {
pub fn new_with_grid_client(client: GridClient) -> Result<Self, Error> {
Ok(Self {
sender: client.agent_throttle_sender(),
..Self::default()
})
}
pub fn new_with_bytes_int32(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
let pos = usize::try_from(pos).map_err(|_| Error::Argument)?;
let end = pos.checked_add(28).ok_or(Error::Argument)?;
let bytes = data.get(pos..end).ok_or(Error::IndexOutOfRange)?;
let mut value = Self::default();
value.set_resend(read_f32(bytes, 0)?);
value.set_land(read_f32(bytes, 4)?);
value.set_wind(read_f32(bytes, 8)?);
value.set_cloud(read_f32(bytes, 12)?);
value.set_task(read_f32(bytes, 16)?);
value.set_texture(read_f32(bytes, 20)?);
value.set_asset(read_f32(bytes, 24)?);
Ok(value)
}
#[must_use]
pub fn with_sender(mut self, sender: Arc<dyn AgentThrottleSender>) -> Self {
self.sender = Some(sender);
self
}
pub fn set_with_method(&self) -> Result<(), Error> {
if let Some(sender) = &self.sender {
sender.send_throttle(&self.to_bytes()?, None)?;
}
Ok(())
}
pub fn set_with_simulator(&self, simulator: Option<Simulator>) -> Result<(), Error> {
if let (Some(sender), Some(simulator)) = (&self.sender, simulator.as_ref()) {
sender.send_throttle(&self.to_bytes()?, Some(simulator))?;
}
Ok(())
}
pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
let mut output = Vec::with_capacity(28);
for value in [
self.resend,
self.land,
self.wind,
self.cloud,
self.task,
self.texture,
self.asset,
] {
output.extend_from_slice(&value.to_le_bytes());
}
Ok(output)
}
#[must_use]
pub const fn asset(&self) -> f32 {
self.asset
}
pub fn set_asset(&mut self, value: f32) {
self.asset = value.clamp(10_000.0, 220_000.0);
}
#[must_use]
pub const fn cloud(&self) -> f32 {
self.cloud
}
pub fn set_cloud(&mut self, value: f32) {
self.cloud = value.clamp(0.0, 34_000.0);
}
#[must_use]
pub const fn land(&self) -> f32 {
self.land
}
pub fn set_land(&mut self, value: f32) {
self.land = value.clamp(0.0, 170_000.0);
}
#[must_use]
pub const fn resend(&self) -> f32 {
self.resend
}
pub fn set_resend(&mut self, value: f32) {
self.resend = value.clamp(10_000.0, 150_000.0);
}
#[must_use]
pub const fn task(&self) -> f32 {
self.task
}
pub fn set_task(&mut self, value: f32) {
self.task = value.clamp(4_000.0, 1_338_000.0);
}
#[must_use]
pub const fn texture(&self) -> f32 {
self.texture
}
pub fn set_texture(&mut self, value: f32) {
self.texture = value.clamp(4_000.0, 446_000.0);
}
#[must_use]
pub fn total(&self) -> f32 {
self.resend + self.land + self.wind + self.cloud + self.task + self.texture + self.asset
}
pub fn set_total(&mut self, value: f32) {
self.set_resend(value * 0.1);
self.set_land(value * 0.52 / 3.0);
self.set_wind(value * 0.05);
self.set_cloud(value * 0.05);
self.set_task(value * 0.704 / 3.0);
self.set_texture(value * 0.704 / 3.0);
self.set_asset(value * 0.484 / 3.0);
}
#[must_use]
pub const fn wind(&self) -> f32 {
self.wind
}
pub fn set_wind(&mut self, value: f32) {
self.wind = value.clamp(0.0, 34_000.0);
}
}
impl Default for AgentThrottle {
fn default() -> Self {
let mut value = Self {
resend: 0.0,
land: 0.0,
wind: 0.0,
cloud: 0.0,
task: 0.0,
texture: 0.0,
asset: 0.0,
sender: None,
};
value.set_total(1_536_000.0);
value
}
}
impl fmt::Debug for AgentThrottle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AgentThrottle")
.field("resend", &self.resend)
.field("land", &self.land)
.field("wind", &self.wind)
.field("cloud", &self.cloud)
.field("task", &self.task)
.field("texture", &self.texture)
.field("asset", &self.asset)
.finish_non_exhaustive()
}
}
fn read_f32(bytes: &[u8], offset: usize) -> Result<f32, Error> {
Ok(f32::from_le_bytes(
bytes
.get(offset..offset + 4)
.ok_or(Error::IndexOutOfRange)?
.try_into()
.map_err(|_| Error::IndexOutOfRange)?,
))
}
/// Outgoing categories used by the native token buckets.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum UdpThrottleCategory {
Unthrottled,
Task,
Texture,
Asset,
}
impl UdpThrottleCategory {
#[must_use]
pub const fn classify(packet_type: PacketType) -> Self {
match packet_type {
PacketType::UseCircuitCode
| PacketType::CompleteAgentMovement
| PacketType::AgentThrottle
| PacketType::LogoutRequest
| PacketType::PacketAck
| PacketType::StartPingCheck
| PacketType::CompletePingCheck
| PacketType::CloseCircuit => Self::Unthrottled,
PacketType::RequestImage => Self::Texture,
PacketType::TransferRequest | PacketType::AbortXfer => Self::Asset,
_ => Self::Task,
}
}
}
/// Bounded transport policy. Defaults match the C# settings used by a client
/// simulator, with explicit limits for queues and per-peer reliable state.
#[derive(Clone, Debug)]
pub struct UdpTransportConfig {
pub receive_queue_capacity: usize,
pub command_queue_capacity: usize,
pub write_queue_capacity: usize,
pub packet_archive_size: usize,
pub pending_ack_capacity: usize,
pub max_pending_acks: usize,
pub reliable_window_capacity: usize,
pub max_resend_count: u32,
pub resend_timeout: Duration,
pub network_tick_interval: Duration,
pub max_datagram_size: usize,
pub max_decoded_packet_size: usize,
pub protocol_mtu: usize,
pub max_peers: usize,
pub throttle: AgentThrottle,
}
impl Default for UdpTransportConfig {
fn default() -> Self {
Self {
receive_queue_capacity: 512,
command_queue_capacity: 512,
write_queue_capacity: 512,
packet_archive_size: 1000,
pending_ack_capacity: 255,
max_pending_acks: 10,
reliable_window_capacity: 1024,
max_resend_count: 3,
resend_timeout: Duration::from_secs(4),
network_tick_interval: Duration::from_millis(500),
max_datagram_size: usize::try_from(UDPPacketBuffer::DEFAULT_BUFFER_SIZE)
.unwrap_or(4096),
max_decoded_packet_size: DEFAULT_DECODE_BUFFER_SIZE,
protocol_mtu: usize::try_from(Packet::MTU).unwrap_or(1200),
max_peers: 32,
throttle: AgentThrottle::default(),
}
}
}
impl UdpTransportConfig {
fn validate(&self) -> Result<(), UdpTransportError> {
for (name, value) in [
("receive_queue_capacity", self.receive_queue_capacity),
("command_queue_capacity", self.command_queue_capacity),
("write_queue_capacity", self.write_queue_capacity),
("packet_archive_size", self.packet_archive_size),
("pending_ack_capacity", self.pending_ack_capacity),
("max_pending_acks", self.max_pending_acks),
("reliable_window_capacity", self.reliable_window_capacity),
("max_datagram_size", self.max_datagram_size),
("max_decoded_packet_size", self.max_decoded_packet_size),
("protocol_mtu", self.protocol_mtu),
("max_peers", self.max_peers),
] {
if value == 0 {
return Err(UdpTransportError::InvalidConfiguration(name));
}
}
if self.pending_ack_capacity > usize::from(u8::MAX) {
return Err(UdpTransportError::InvalidConfiguration(
"pending_ack_capacity",
));
}
if self.max_pending_acks > self.pending_ack_capacity {
return Err(UdpTransportError::InvalidConfiguration("max_pending_acks"));
}
if self.protocol_mtu < 10 || self.protocol_mtu > self.max_datagram_size {
return Err(UdpTransportError::InvalidConfiguration("protocol_mtu"));
}
if self.max_decoded_packet_size < self.max_datagram_size {
return Err(UdpTransportError::InvalidConfiguration(
"max_decoded_packet_size",
));
}
if self.resend_timeout.is_zero() {
return Err(UdpTransportError::InvalidConfiguration("resend_timeout"));
}
if self.network_tick_interval.is_zero() {
return Err(UdpTransportError::InvalidConfiguration(
"network_tick_interval",
));
}
Ok(())
}
}
/// Immutable, redacted snapshot of transport counters.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct UdpTransportStats {
pub received_datagrams: u64,
pub received_bytes: u64,
pub sent_datagrams: u64,
pub sent_bytes: u64,
pub dropped_receive_queue: u64,
pub dropped_send_queue: u64,
pub malformed_datagrams: u64,
pub rejected_sources: u64,
pub duplicate_datagrams: u64,
pub out_of_order_datagrams: u64,
pub sequence_gaps: u64,
pub acknowledgements_received: u64,
pub acknowledgements_sent: u64,
pub resent_datagrams: u64,
pub failed_resends: u64,
pub socket_errors: u64,
pub handler_panics: u64,
}
#[derive(Default)]
struct StatsCounters {
received_datagrams: AtomicU64,
received_bytes: AtomicU64,
sent_datagrams: AtomicU64,
sent_bytes: AtomicU64,
dropped_receive_queue: AtomicU64,
dropped_send_queue: AtomicU64,
malformed_datagrams: AtomicU64,
rejected_sources: AtomicU64,
duplicate_datagrams: AtomicU64,
out_of_order_datagrams: AtomicU64,
sequence_gaps: AtomicU64,
acknowledgements_received: AtomicU64,
acknowledgements_sent: AtomicU64,
resent_datagrams: AtomicU64,
failed_resends: AtomicU64,
socket_errors: AtomicU64,
handler_panics: AtomicU64,
}
impl StatsCounters {
fn snapshot(&self) -> UdpTransportStats {
let load = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
UdpTransportStats {
received_datagrams: load(&self.received_datagrams),
received_bytes: load(&self.received_bytes),
sent_datagrams: load(&self.sent_datagrams),
sent_bytes: load(&self.sent_bytes),
dropped_receive_queue: load(&self.dropped_receive_queue),
dropped_send_queue: load(&self.dropped_send_queue),
malformed_datagrams: load(&self.malformed_datagrams),
rejected_sources: load(&self.rejected_sources),
duplicate_datagrams: load(&self.duplicate_datagrams),
out_of_order_datagrams: load(&self.out_of_order_datagrams),
sequence_gaps: load(&self.sequence_gaps),
acknowledgements_received: load(&self.acknowledgements_received),
acknowledgements_sent: load(&self.acknowledgements_sent),
resent_datagrams: load(&self.resent_datagrams),
failed_resends: load(&self.failed_resends),
socket_errors: load(&self.socket_errors),
handler_panics: load(&self.handler_panics),
}
}
}
/// Callback boundary corresponding to the protected methods on C# `UDPBase`.
pub trait UdpPacketHandler: Send + Sync + 'static {
fn packet_received(&self, _buffer: UDPPacketBuffer) {}
fn packet_sent(&self, _buffer: UDPPacketBuffer, _bytes_sent: usize) {}
fn packet_dropped(&self) {}
}
struct NoopPacketHandler;
impl UdpPacketHandler for NoopPacketHandler {}
struct RuntimeTasks {
cancellation: CancellationTokenSource,
commands: mpsc::Sender<CoordinatorCommand>,
handles: Vec<JoinHandle<()>>,
local_address: SocketAddr,
}
struct UdpInner {
bind_address: SocketAddr,
remote_end_point: Option<SocketAddr>,
config: UdpTransportConfig,
handler: Arc<dyn UdpPacketHandler>,
parent_cancellation: CancellationToken,
running: AtomicBool,
runtime: Mutex<Option<RuntimeTasks>>,
stats: Arc<StatsCounters>,
}
impl Drop for UdpInner {
fn drop(&mut self) {
if let Some(runtime) = self
.runtime
.get_mut()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
runtime.cancellation.cancel();
for handle in runtime.handles {
handle.abort();
}
}
}
}
/// Runtime-owning native implementation of the C# UDP transport slice.
///
/// Construction creates no socket and no runtime. `start` binds a nonblocking
/// socket and spawns tasks on the caller's current Tokio runtime.
#[derive(Clone)]
pub struct UDPBase {
inner: Arc<UdpInner>,
}
impl UDPBase {
pub fn client(
remote_end_point: SocketAddr,
config: UdpTransportConfig,
handler: Arc<dyn UdpPacketHandler>,
parent_cancellation: CancellationToken,
) -> Result<Self, UdpTransportError> {
let bind_address = match remote_end_point.ip() {
IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
};
Self::new(
bind_address,
Some(remote_end_point),
config,
handler,
parent_cancellation,
)
}
pub fn server(
bind_address: SocketAddr,
config: UdpTransportConfig,
handler: Arc<dyn UdpPacketHandler>,
parent_cancellation: CancellationToken,
) -> Result<Self, UdpTransportError> {
Self::new(bind_address, None, config, handler, parent_cancellation)
}
pub fn client_with_defaults(remote_end_point: SocketAddr) -> Result<Self, UdpTransportError> {
Self::client(
remote_end_point,
UdpTransportConfig::default(),
Arc::new(NoopPacketHandler),
CancellationToken::default(),
)
}
fn new(
bind_address: SocketAddr,
remote_end_point: Option<SocketAddr>,
config: UdpTransportConfig,
handler: Arc<dyn UdpPacketHandler>,
parent_cancellation: CancellationToken,
) -> Result<Self, UdpTransportError> {
config.validate()?;
Ok(Self {
inner: Arc::new(UdpInner {
bind_address,
remote_end_point,
config,
handler,
parent_cancellation,
running: AtomicBool::new(false),
runtime: Mutex::new(None),
stats: Arc::new(StatsCounters::default()),
}),
})
}
pub fn start(&self) -> Result<(), Error> {
self.start_transport().map_err(Into::into)
}
pub fn start_transport(&self) -> Result<(), UdpTransportError> {
let mut runtime = self
.inner
.runtime
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if runtime.is_some() {
return Ok(());
}
if self.inner.parent_cancellation.is_cancellation_requested() {
return Err(UdpTransportError::Cancelled);
}
let handle = tokio::runtime::Handle::try_current()
.map_err(|_| UdpTransportError::RuntimeUnavailable)?;
let std_socket = std::net::UdpSocket::bind(self.inner.bind_address)
.map_err(|_| UdpTransportError::Socket)?;
std_socket
.set_nonblocking(true)
.map_err(|_| UdpTransportError::Socket)?;
let local_address = std_socket
.local_addr()
.map_err(|_| UdpTransportError::Socket)?;
let socket =
Arc::new(UdpSocket::from_std(std_socket).map_err(|_| UdpTransportError::Socket)?);
let cancellation = CancellationTokenSource::new_linked(std::slice::from_ref(
&self.inner.parent_cancellation,
));
let token = cancellation.token();
let (raw_sender, raw_receiver) = mpsc::channel(self.inner.config.receive_queue_capacity);
let (command_sender, command_receiver) =
mpsc::channel(self.inner.config.command_queue_capacity);
let (write_sender, write_receiver) = mpsc::channel(self.inner.config.write_queue_capacity);
let receiver_handle = handle.spawn(receive_loop(
Arc::clone(&socket),
raw_sender,
token.clone(),
self.inner.config.max_datagram_size,
Arc::clone(&self.inner.stats),
Arc::clone(&self.inner.handler),
));
let coordinator_handle = handle.spawn(coordinator_loop(
raw_receiver,
command_receiver,
write_sender,
token.clone(),
self.inner.remote_end_point,
self.inner.config.clone(),
Arc::clone(&self.inner.stats),
Arc::clone(&self.inner.handler),
));
let writer_handle = handle.spawn(writer_loop(
socket,
write_receiver,
token,
self.inner.config.throttle.clone(),
Arc::clone(&self.inner.stats),
Arc::clone(&self.inner.handler),
));
*runtime = Some(RuntimeTasks {
cancellation,
commands: command_sender,
handles: vec![receiver_handle, coordinator_handle, writer_handle],
local_address,
});
self.inner.running.store(true, Ordering::Release);
Ok(())
}
pub fn stop(&self) -> Result<(), Error> {
self.stop_transport();
Ok(())
}
pub fn stop_transport(&self) {
let runtime = self
.inner
.runtime
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.inner.running.store(false, Ordering::Release);
if let Some(runtime) = runtime {
runtime.cancellation.cancel();
for handle in runtime.handles {
handle.abort();
}
}
}
pub async fn stop_async(&self) {
let runtime = self
.inner
.runtime
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.inner.running.store(false, Ordering::Release);
if let Some(runtime) = runtime {
runtime.cancellation.cancel();
drop(runtime.commands);
for handle in runtime.handles {
let _ = handle.await;
}
}
}
#[must_use]
pub fn is_running(&self) -> bool {
self.inner.running.load(Ordering::Acquire)
}
#[must_use]
pub fn local_address(&self) -> Option<SocketAddr> {
self.inner
.runtime
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map(|runtime| runtime.local_address)
}
#[must_use]
pub fn stats(&self) -> UdpTransportStats {
self.inner.stats.snapshot()
}
pub fn async_begin_send(&self, buf: UDPPacketBuffer) -> Result<(), Error> {
let Some(sender) = self.command_sender() else {
return Ok(());
};
sender
.try_send(CoordinatorCommand::Raw(buf))
.map_err(|error| {
if matches!(error, mpsc::error::TrySendError::Full(_)) {
self.inner
.stats
.dropped_send_queue
.fetch_add(1, Ordering::Relaxed);
Error::InvalidOperation
} else {
Error::Cancelled
}
})
}
pub async fn send_packet(
&self,
data: Vec<u8>,
packet_type: PacketType,
do_zerocode: bool,
cancellation: CancellationToken,
) -> Result<u32, UdpTransportError> {
let destination = self
.inner
.remote_end_point
.ok_or(UdpTransportError::InvalidBuffer)?;
self.send_packet_to(data, destination, packet_type, do_zerocode, cancellation)
.await
}
pub async fn send_packet_to(
&self,
data: Vec<u8>,
destination: SocketAddr,
packet_type: PacketType,
do_zerocode: bool,
cancellation: CancellationToken,
) -> Result<u32, UdpTransportError> {
let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?;
let (response_sender, response_receiver) = oneshot::channel();
let command = CoordinatorCommand::Packet {
data,
destination,
packet_type,
do_zerocode,
response: response_sender,
acknowledgement: None,
};
tokio::select! {
result = sender.send(command) => {
result.map_err(|_| UdpTransportError::Cancelled)?;
}
() = cancellation.cancelled() => return Err(UdpTransportError::Cancelled),
() = self.inner.parent_cancellation.cancelled() => {
return Err(UdpTransportError::Cancelled);
}
}
tokio::select! {
result = response_receiver => result.map_err(|_| UdpTransportError::Cancelled)?,
() = cancellation.cancelled() => Err(UdpTransportError::Cancelled),
() = self.inner.parent_cancellation.cancelled() => Err(UdpTransportError::Cancelled),
}
}
pub fn try_send_packet(
&self,
data: Vec<u8>,
destination: SocketAddr,
packet_type: PacketType,
do_zerocode: bool,
) -> Result<oneshot::Receiver<Result<u32, UdpTransportError>>, UdpTransportError> {
let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?;
let (response, receiver) = oneshot::channel();
sender
.try_send(CoordinatorCommand::Packet {
data,
destination,
packet_type,
do_zerocode,
response,
acknowledgement: None,
})
.map_err(|error| {
if matches!(error, mpsc::error::TrySendError::Full(_)) {
self.inner
.stats
.dropped_send_queue
.fetch_add(1, Ordering::Relaxed);
UdpTransportError::Backpressure
} else {
UdpTransportError::Cancelled
}
})?;
Ok(receiver)
}
/// Queues a packet and resolves only after its reliable sequence is
/// acknowledged by the peer. The receiver disconnects if the reliable
/// packet is dropped or the transport shuts down.
pub(crate) fn try_send_packet_wait_ack(
&self,
data: Vec<u8>,
destination: SocketAddr,
packet_type: PacketType,
do_zerocode: bool,
) -> Result<AckReceiver<()>, UdpTransportError> {
let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?;
let (response, _queued) = oneshot::channel();
let (acknowledgement, receiver) = sync_channel(1);
sender
.try_send(CoordinatorCommand::Packet {
data,
destination,
packet_type,
do_zerocode,
response,
acknowledgement: Some(acknowledgement),
})
.map_err(|error| {
if matches!(error, mpsc::error::TrySendError::Full(_)) {
self.inner
.stats
.dropped_send_queue
.fetch_add(1, Ordering::Relaxed);
UdpTransportError::Backpressure
} else {
UdpTransportError::Cancelled
}
})?;
Ok(receiver)
}
/// Queues an immediate flush of acknowledgements pending for `destination`.
///
/// The network manager uses this for the protocol's `OldestUnacked`
/// request in `StartPingCheck`, matching `Simulator.SendAcks()` without
/// exposing coordinator state or blocking the packet callback thread.
pub(crate) fn try_flush_acks(&self, destination: SocketAddr) -> Result<(), UdpTransportError> {
let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?;
sender
.try_send(CoordinatorCommand::FlushAcks { destination })
.map_err(|error| {
if matches!(error, mpsc::error::TrySendError::Full(_)) {
self.inner
.stats
.dropped_send_queue
.fetch_add(1, Ordering::Relaxed);
UdpTransportError::Backpressure
} else {
UdpTransportError::Cancelled
}
})
}
pub fn update_throttle(&self, throttle: AgentThrottle) -> Result<(), UdpTransportError> {
let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?;
sender
.try_send(CoordinatorCommand::UpdateThrottle(throttle))
.map_err(|error| {
if matches!(error, mpsc::error::TrySendError::Full(_)) {
UdpTransportError::Backpressure
} else {
UdpTransportError::Cancelled
}
})
}
fn command_sender(&self) -> Option<mpsc::Sender<CoordinatorCommand>> {
self.inner
.runtime
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map(|runtime| runtime.commands.clone())
}
}
impl fmt::Debug for UDPBase {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("UDPBase")
.field(
"mode",
&self.inner.remote_end_point.map_or("server", |_| "client"),
)
.field("is_running", &self.is_running())
.field("stats", &self.stats())
.finish_non_exhaustive()
}
}
enum CoordinatorCommand {
Raw(UDPPacketBuffer),
Packet {
data: Vec<u8>,
destination: SocketAddr,
packet_type: PacketType,
do_zerocode: bool,
response: oneshot::Sender<Result<u32, UdpTransportError>>,
acknowledgement: Option<AckSender<()>>,
},
FlushAcks {
destination: SocketAddr,
},
UpdateThrottle(AgentThrottle),
}
enum WriteCommand {
Datagram {
buffer: UDPPacketBuffer,
category: UdpThrottleCategory,
},
UpdateThrottle(AgentThrottle),
}
struct ReliablePacket {
buffer: UDPPacketBuffer,
category: UdpThrottleCategory,
last_sent: Instant,
resend_count: u32,
acknowledgement: Option<AckSender<()>>,
}
struct PeerState {
sequence: u32,
archive: IncomingPacketIDCollection,
pending_acks: VecDeque<u32>,
need_ack: BTreeMap<u32, ReliablePacket>,
latest_received: Option<u32>,
}
impl PeerState {
fn new(config: &UdpTransportConfig) -> Result<Self, UdpTransportError> {
Ok(Self {
sequence: 0,
archive: IncomingPacketIDCollection::new(
i32::try_from(config.packet_archive_size)
.map_err(|_| UdpTransportError::InvalidConfiguration("packet_archive_size"))?,
)
.map_err(|_| UdpTransportError::InvalidConfiguration("packet_archive_size"))?,
pending_acks: VecDeque::with_capacity(config.pending_ack_capacity),
need_ack: BTreeMap::new(),
latest_received: None,
})
}
fn next_sequence(&mut self) -> u32 {
// C# increments an Int32 and casts it to UInt32 for the four-byte
// header. The equivalent wire sequence spans all u32 values and wraps
// through zero.
self.sequence = self.sequence.wrapping_add(1);
self.sequence
}
fn observe_sequence(&mut self, sequence: u32, stats: &StatsCounters) {
let Some(latest) = self.latest_received else {
self.latest_received = Some(sequence);
return;
};
let expected = latest.wrapping_add(1);
if sequence == expected {
self.latest_received = Some(sequence);
return;
}
let forward = sequence.wrapping_sub(latest);
if forward != 0 && forward <= (u32::MAX / 2) + 1 {
stats.sequence_gaps.fetch_add(1, Ordering::Relaxed);
self.latest_received = Some(sequence);
} else {
stats.out_of_order_datagrams.fetch_add(1, Ordering::Relaxed);
}
}
}
struct CoordinatorState {
peers: HashMap<SocketAddr, PeerState>,
remote_end_point: Option<SocketAddr>,
config: UdpTransportConfig,
stats: Arc<StatsCounters>,
}
impl CoordinatorState {
fn canonical_peer(&self, source: SocketAddr) -> Result<SocketAddr, UdpTransportError> {
if let Some(remote) = self.remote_end_point {
if remote.ip() != source.ip() {
return Err(UdpTransportError::InvalidBuffer);
}
Ok(remote)
} else {
Ok(source)
}
}
fn peer_mut(&mut self, endpoint: SocketAddr) -> Result<&mut PeerState, UdpTransportError> {
if !self.peers.contains_key(&endpoint) {
if self.peers.len() >= self.config.max_peers {
return Err(UdpTransportError::TooManyPeers);
}
let peer = PeerState::new(&self.config)?;
self.peers.insert(endpoint, peer);
}
self.peers
.get_mut(&endpoint)
.ok_or(UdpTransportError::TooManyPeers)
}
fn prepare_packet(
&mut self,
data: Vec<u8>,
destination: SocketAddr,
packet_type: PacketType,
do_zerocode: bool,
acknowledgement: Option<AckSender<()>>,
) -> Result<(WriteCommand, u32), UdpTransportError> {
let mut data = encode_for_transport(data, do_zerocode, self.config.protocol_mtu)?;
if data.len() < 6 {
return Err(UdpTransportError::InvalidBuffer);
}
let reliable = data[0] & Helpers::MSG_RELIABLE != 0;
let category = UdpThrottleCategory::classify(packet_type);
let reliable_capacity = self.config.reliable_window_capacity;
let protocol_mtu = self.config.protocol_mtu;
let peer = self.peer_mut(destination)?;
if reliable && peer.need_ack.len() >= reliable_capacity {
return Err(UdpTransportError::ReliableWindowFull);
}
append_pending_acks(&mut data, &mut peer.pending_acks, protocol_mtu)?;
let sequence = peer.next_sequence();
data[1..5].copy_from_slice(&sequence.to_be_bytes());
let data_length =
i32::try_from(data.len()).map_err(|_| UdpTransportError::InvalidBuffer)?;
let buffer = UDPPacketBuffer {
data,
data_length,
remote_end_point: destination,
};
if reliable {
peer.need_ack.insert(
sequence,
ReliablePacket {
buffer: buffer.clone(),
category,
last_sent: Instant::now(),
resend_count: 0,
acknowledgement,
},
);
} else if let Some(acknowledgement) = acknowledgement {
let _ = acknowledgement.try_send(());
}
Ok((WriteCommand::Datagram { buffer, category }, sequence))
}
fn take_resends(&mut self) -> Vec<WriteCommand> {
let now = Instant::now();
let mut writes = Vec::new();
for peer in self.peers.values_mut() {
let mut failed = Vec::new();
for (&sequence, packet) in &mut peer.need_ack {
if now.duration_since(packet.last_sent) <= self.config.resend_timeout {
continue;
}
if packet.resend_count < self.config.max_resend_count {
packet.resend_count += 1;
packet.last_sent = now;
if let Some(flags) = packet.buffer.data.first_mut() {
*flags |= Helpers::MSG_RESENT;
}
self.stats.resent_datagrams.fetch_add(1, Ordering::Relaxed);
writes.push(WriteCommand::Datagram {
buffer: packet.buffer.clone(),
category: packet.category,
});
} else {
failed.push(sequence);
}
}
for sequence in failed {
peer.need_ack.remove(&sequence);
self.stats.failed_resends.fetch_add(1, Ordering::Relaxed);
}
}
writes
}
}
fn encode_for_transport(
mut data: Vec<u8>,
do_zerocode: bool,
mtu: usize,
) -> Result<Vec<u8>, UdpTransportError> {
if data.len() < 6 {
return Err(UdpTransportError::InvalidBuffer);
}
if data.len() > mtu {
return Err(UdpTransportError::MtuExceeded);
}
if !do_zerocode {
return Ok(data);
}
data[0] |= Helpers::MSG_ZEROCODED;
let mut encoded = vec![0_u8; mtu];
let source_length = i32::try_from(data.len()).map_err(|_| UdpTransportError::InvalidBuffer)?;
match crate::packet_wire::zero_encode(Some(&data), source_length, Some(&mut encoded)) {
Ok(length) => {
let length = usize::try_from(length).map_err(|_| UdpTransportError::InvalidBuffer)?;
encoded.truncate(length);
Ok(encoded)
}
Err(Error::IndexOutOfRange) => {
data[0] &= !Helpers::MSG_ZEROCODED;
Ok(data)
}
Err(_) => Err(UdpTransportError::InvalidBuffer),
}
}
fn append_pending_acks(
data: &mut Vec<u8>,
pending: &mut VecDeque<u32>,
mtu: usize,
) -> Result<(), UdpTransportError> {
if data
.first()
.is_some_and(|flags| flags & Helpers::MSG_APPENDED_ACKS != 0)
{
return Err(UdpTransportError::InvalidBuffer);
}
let mut count = 0_u8;
while data.len().checked_add(5).is_some_and(|length| length < mtu) {
let Some(ack) = pending.pop_front() else {
break;
};
data.extend_from_slice(&ack.to_be_bytes());
count = count.saturating_add(1);
if count == u8::MAX {
break;
}
}
if count != 0 {
data.push(count);
data[0] |= Helpers::MSG_APPENDED_ACKS;
}
Ok(())
}
async fn receive_loop(
socket: Arc<UdpSocket>,
sender: mpsc::Sender<UDPPacketBuffer>,
cancellation: CancellationToken,
max_datagram_size: usize,
stats: Arc<StatsCounters>,
handler: Arc<dyn UdpPacketHandler>,
) {
let mut storage = vec![0_u8; max_datagram_size.saturating_add(1)];
loop {
let received = tokio::select! {
() = cancellation.cancelled() => break,
result = socket.recv_from(&mut storage) => result,
};
let Ok((length, source)) = received else {
if !cancellation.is_cancellation_requested() {
stats.socket_errors.fetch_add(1, Ordering::Relaxed);
}
break;
};
stats.received_datagrams.fetch_add(1, Ordering::Relaxed);
stats
.received_bytes
.fetch_add(u64::try_from(length).unwrap_or(u64::MAX), Ordering::Relaxed);
if length > max_datagram_size {
stats.malformed_datagrams.fetch_add(1, Ordering::Relaxed);
continue;
}
let packet = UDPPacketBuffer {
data: storage[..length].to_vec(),
data_length: i32::try_from(length).unwrap_or(i32::MAX),
remote_end_point: source,
};
if sender.try_send(packet).is_err() {
stats.dropped_receive_queue.fetch_add(1, Ordering::Relaxed);
invoke_handler(&stats, || handler.packet_dropped());
}
}
}
#[allow(clippy::too_many_arguments)] // Each argument is one explicit task ownership boundary.
async fn coordinator_loop(
mut incoming: mpsc::Receiver<UDPPacketBuffer>,
mut commands: mpsc::Receiver<CoordinatorCommand>,
writer: mpsc::Sender<WriteCommand>,
cancellation: CancellationToken,
remote_end_point: Option<SocketAddr>,
config: UdpTransportConfig,
stats: Arc<StatsCounters>,
handler: Arc<dyn UdpPacketHandler>,
) {
let mut coordinator = CoordinatorState {
peers: HashMap::new(),
remote_end_point,
config: config.clone(),
stats: Arc::clone(&stats),
};
let mut tick = tokio::time::interval(config.network_tick_interval);
tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
tick.tick().await;
loop {
tokio::select! {
biased;
() = cancellation.cancelled() => break,
Some(buffer) = incoming.recv() => {
process_incoming(&mut coordinator, buffer, &writer, &handler).await;
}
Some(command) = commands.recv() => {
process_command(&mut coordinator, command, &writer).await;
}
_ = tick.tick() => {
flush_pending_acks(&mut coordinator, &writer).await;
for resend in coordinator.take_resends() {
if writer.send(resend).await.is_err() {
return;
}
}
}
else => break,
}
}
}
async fn process_command(
state: &mut CoordinatorState,
command: CoordinatorCommand,
writer: &mpsc::Sender<WriteCommand>,
) {
match command {
CoordinatorCommand::Raw(buffer) => {
let valid = buffer.payload().is_ok()
&& usize::try_from(buffer.data_length)
.is_ok_and(|length| length <= state.config.max_datagram_size);
if !valid {
state
.stats
.dropped_send_queue
.fetch_add(1, Ordering::Relaxed);
return;
}
let _ = writer
.send(WriteCommand::Datagram {
buffer,
category: UdpThrottleCategory::Unthrottled,
})
.await;
}
CoordinatorCommand::Packet {
data,
destination,
packet_type,
do_zerocode,
response,
acknowledgement,
} => {
let result =
state.prepare_packet(data, destination, packet_type, do_zerocode, acknowledgement);
let result = match result {
Ok((write, sequence)) => writer
.send(write)
.await
.map(|()| sequence)
.map_err(|_| UdpTransportError::Cancelled),
Err(error) => Err(error),
};
let _ = response.send(result);
}
CoordinatorCommand::FlushAcks { destination } => {
send_peer_acks(state, destination, writer).await;
}
CoordinatorCommand::UpdateThrottle(throttle) => {
let _ = writer.send(WriteCommand::UpdateThrottle(throttle)).await;
}
}
}
async fn process_incoming(
state: &mut CoordinatorState,
buffer: UDPPacketBuffer,
writer: &mpsc::Sender<WriteCommand>,
handler: &Arc<dyn UdpPacketHandler>,
) {
let Ok(endpoint) = state.canonical_peer(buffer.remote_end_point) else {
state.stats.rejected_sources.fetch_add(1, Ordering::Relaxed);
return;
};
let Ok(payload) = buffer.payload() else {
state
.stats
.malformed_datagrams
.fetch_add(1, Ordering::Relaxed);
return;
};
let Ok(payload_length) = i32::try_from(payload.len()) else {
state
.stats
.malformed_datagrams
.fetch_add(1, Ordering::Relaxed);
return;
};
let mut packet_end = payload_length - 1;
let mut zero_buffer = vec![0_u8; state.config.max_decoded_packet_size];
let Ok(packet) =
crate::packet_wire::build_packet_from_bytes(payload, &mut packet_end, &mut zero_buffer)
else {
state
.stats
.malformed_datagrams
.fetch_add(1, Ordering::Relaxed);
return;
};
let appended_acks = packet.header.ack_list.clone().unwrap_or_default();
let mut standalone_acks = Vec::new();
if packet.type_ == PacketType::PacketAck {
let mut position = 0;
if let Ok(ack_packet) =
PacketAckPacket::new_with_bytes_int32(payload.to_vec(), &mut position)
{
standalone_acks.extend(ack_packet.packets.into_iter().map(|block| block.id));
} else {
state
.stats
.malformed_datagrams
.fetch_add(1, Ordering::Relaxed);
return;
}
}
let pending_threshold = state.config.max_pending_acks;
let pending_capacity = state.config.pending_ack_capacity;
let counters = Arc::clone(&state.stats);
let Ok(peer) = state.peer_mut(endpoint) else {
counters.rejected_sources.fetch_add(1, Ordering::Relaxed);
return;
};
for ack in appended_acks.into_iter().chain(standalone_acks) {
if let Some(mut reliable) = peer.need_ack.remove(&ack)
&& let Some(acknowledgement) = reliable.acknowledgement.take()
{
let _ = acknowledgement.try_send(());
}
counters
.acknowledgements_received
.fetch_add(1, Ordering::Relaxed);
}
if packet.header.reliable {
if peer.pending_acks.len() < pending_capacity {
peer.pending_acks.push_back(packet.header.sequence);
}
if !peer.archive.try_enqueue(packet.header.sequence) {
counters.duplicate_datagrams.fetch_add(1, Ordering::Relaxed);
if peer.pending_acks.len() >= pending_threshold {
send_peer_acks(state, endpoint, writer).await;
}
return;
}
peer.observe_sequence(packet.header.sequence, &counters);
}
if state
.peers
.get(&endpoint)
.is_some_and(|peer| peer.pending_acks.len() >= pending_threshold)
{
send_peer_acks(state, endpoint, writer).await;
}
invoke_handler(&state.stats, || handler.packet_received(buffer));
}
async fn flush_pending_acks(state: &mut CoordinatorState, writer: &mpsc::Sender<WriteCommand>) {
let endpoints: Vec<_> = state
.peers
.iter()
.filter_map(|(endpoint, peer)| (!peer.pending_acks.is_empty()).then_some(*endpoint))
.collect();
for endpoint in endpoints {
send_peer_acks(state, endpoint, writer).await;
}
}
async fn send_peer_acks(
state: &mut CoordinatorState,
endpoint: SocketAddr,
writer: &mpsc::Sender<WriteCommand>,
) {
let Some(peer) = state.peers.get_mut(&endpoint) else {
return;
};
if peer.pending_acks.is_empty() {
return;
}
let mut blocks = Vec::with_capacity(peer.pending_acks.len());
while let Some(id) = peer.pending_acks.pop_front() {
blocks.push(PacketAckPacketPacketsBlock { id });
}
let ack_count = blocks.len();
let Ok(mut packet) = PacketAckPacket::new_with_constructor() else {
return;
};
packet.packets = blocks;
let bytes = match packet.to_bytes_with_method() {
Ok(mut bytes) => {
// C# `SendAcks` explicitly marks standalone PacketAck packets as
// unreliable before serialization.
if let Some(flags) = bytes.first_mut() {
*flags &= !Helpers::MSG_RELIABLE;
}
bytes
}
Err(_) => return,
};
let Ok((write, _)) = state.prepare_packet(bytes, endpoint, PacketType::PacketAck, false, None)
else {
return;
};
if writer.send(write).await.is_ok() {
state.stats.acknowledgements_sent.fetch_add(
u64::try_from(ack_count).unwrap_or(u64::MAX),
Ordering::Relaxed,
);
}
}
fn invoke_handler(stats: &StatsCounters, callback: impl FnOnce()) {
if catch_unwind(AssertUnwindSafe(callback)).is_err() {
stats.handler_panics.fetch_add(1, Ordering::Relaxed);
}
}
struct TokenBucket {
tokens_per_period: usize,
token_limit: usize,
available: usize,
last_replenishment: Instant,
}
impl TokenBucket {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn new(bits_per_second: f32) -> Self {
let calculated =
(f64::from(bits_per_second) / 8.0 * THROTTLE_PERIOD.as_secs_f64()).max(0.0) as usize;
let tokens_per_period = calculated.max(THROTTLE_MIN_BYTES_PER_PERIOD);
let token_limit = tokens_per_period.saturating_mul(THROTTLE_BURST_PERIODS);
Self {
tokens_per_period,
token_limit,
available: token_limit,
last_replenishment: Instant::now(),
}
}
fn replenish(&mut self, now: Instant) {
let elapsed = now.duration_since(self.last_replenishment);
let periods = elapsed.as_nanos() / THROTTLE_PERIOD.as_nanos();
if periods == 0 {
return;
}
let periods = usize::try_from(periods).unwrap_or(usize::MAX);
self.available = self
.available
.saturating_add(self.tokens_per_period.saturating_mul(periods))
.min(self.token_limit);
let periods_u32 = u32::try_from(periods).unwrap_or(u32::MAX);
self.last_replenishment += THROTTLE_PERIOD.saturating_mul(periods_u32);
}
async fn acquire(
&mut self,
amount: usize,
cancellation: &CancellationToken,
) -> Result<(), UdpTransportError> {
let amount = amount.clamp(1, self.token_limit);
loop {
self.replenish(Instant::now());
if self.available >= amount {
self.available -= amount;
return Ok(());
}
let deficit = amount - self.available;
let periods = deficit.div_ceil(self.tokens_per_period);
let periods = u32::try_from(periods).unwrap_or(u32::MAX);
let deadline = self.last_replenishment + THROTTLE_PERIOD.saturating_mul(periods.max(1));
tokio::select! {
() = cancellation.cancelled() => return Err(UdpTransportError::Cancelled),
() = tokio::time::sleep_until(deadline) => {}
}
}
}
}
struct UdpThrottle {
task: TokenBucket,
texture: TokenBucket,
asset: TokenBucket,
}
impl UdpThrottle {
fn new(throttle: &AgentThrottle) -> Self {
Self {
task: TokenBucket::new(throttle.task()),
texture: TokenBucket::new(throttle.texture()),
asset: TokenBucket::new(throttle.asset()),
}
}
async fn acquire(
&mut self,
category: UdpThrottleCategory,
amount: usize,
cancellation: &CancellationToken,
) -> Result<(), UdpTransportError> {
match category {
UdpThrottleCategory::Unthrottled => Ok(()),
UdpThrottleCategory::Task => self.task.acquire(amount, cancellation).await,
UdpThrottleCategory::Texture => self.texture.acquire(amount, cancellation).await,
UdpThrottleCategory::Asset => self.asset.acquire(amount, cancellation).await,
}
}
}
async fn writer_loop(
socket: Arc<UdpSocket>,
mut receiver: mpsc::Receiver<WriteCommand>,
cancellation: CancellationToken,
initial_throttle: AgentThrottle,
stats: Arc<StatsCounters>,
handler: Arc<dyn UdpPacketHandler>,
) {
let mut throttle = UdpThrottle::new(&initial_throttle);
loop {
let command = tokio::select! {
biased;
() = cancellation.cancelled() => break,
command = receiver.recv() => command,
};
match command {
Some(WriteCommand::UpdateThrottle(values)) => {
throttle = UdpThrottle::new(&values);
}
Some(WriteCommand::Datagram { buffer, category }) => {
let Ok(payload) = buffer.payload() else {
stats.dropped_send_queue.fetch_add(1, Ordering::Relaxed);
continue;
};
if throttle
.acquire(category, payload.len(), &cancellation)
.await
.is_err()
{
break;
}
match socket.send_to(payload, buffer.remote_end_point).await {
Ok(bytes_sent) => {
stats.sent_datagrams.fetch_add(1, Ordering::Relaxed);
stats.sent_bytes.fetch_add(
u64::try_from(bytes_sent).unwrap_or(u64::MAX),
Ordering::Relaxed,
);
invoke_handler(&stats, || handler.packet_sent(buffer, bytes_sent));
}
Err(_) => {
stats.socket_errors.fetch_add(1, Ordering::Relaxed);
}
}
}
None => break,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sequence_rolls_over_across_the_full_reference_header_width() {
let config = UdpTransportConfig::default();
let mut peer = PeerState::new(&config).unwrap();
peer.sequence = u32::MAX - 1;
assert_eq!(peer.next_sequence(), u32::MAX);
assert_eq!(peer.next_sequence(), 0);
assert_eq!(peer.next_sequence(), 1);
}
#[tokio::test(start_paused = true)]
async fn fake_time_drives_token_replenishment_and_reliable_expiry() {
let cancellation = CancellationToken::default();
let mut bucket = TokenBucket::new(4_000.0);
bucket.acquire(800, &cancellation).await.unwrap();
let started = Instant::now();
bucket.acquire(800, &cancellation).await.unwrap();
assert_eq!(
Instant::now().duration_since(started),
Duration::from_millis(400)
);
let config = UdpTransportConfig {
resend_timeout: Duration::from_secs(4),
max_resend_count: 1,
..UdpTransportConfig::default()
};
let mut coordinator = CoordinatorState {
peers: HashMap::new(),
remote_end_point: None,
config,
stats: Arc::new(StatsCounters::default()),
};
let destination: SocketAddr = "127.0.0.1:13000".parse().unwrap();
let packet = vec![Helpers::MSG_RELIABLE, 0, 0, 0, 0, 0, 1];
coordinator
.prepare_packet(packet, destination, PacketType::ObjectUpdate, false, None)
.unwrap();
assert!(coordinator.take_resends().is_empty());
tokio::time::advance(Duration::from_millis(4_001)).await;
let resend = coordinator.take_resends();
assert_eq!(resend.len(), 1);
let WriteCommand::Datagram { buffer, .. } = &resend[0] else {
panic!("expected resend datagram");
};
assert_ne!(buffer.data[0] & Helpers::MSG_RESENT, 0);
assert_eq!(u32::from_be_bytes(buffer.data[1..5].try_into().unwrap()), 1);
tokio::time::advance(Duration::from_millis(4_001)).await;
assert!(coordinator.take_resends().is_empty());
assert_eq!(coordinator.stats.failed_resends.load(Ordering::Relaxed), 1);
assert!(coordinator.peers[&destination].need_ack.is_empty());
}
}