feat(client): implement multi-target ManagedClientGroup for shared-socket probing
Introduce a managed group runner to support concurrent probing of multiple targets over a single UDP socket with coordinated send pacing. Refactor the existing client logic into a reusable SessionRuntime to support both individual and grouped session management.
This commit is contained in:
1 parent
ac82537043
commit
acffc3cd4e
10 files changed
+3126
-817
No files matched your search
@@ -1,36 +1,32 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
net::{SocketAddr, UdpSocket},
|
net::{SocketAddr, UdpSocket},
|
||||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use irtt_proto::{
|
#[cfg(test)]
|
||||||
close::CloseRequest, decode_echo_reply, echo_packet_len, encode_close_request,
|
use irtt_proto::{flags, Params, TimestampFields, PROTOCOL_VERSION};
|
||||||
encode_echo_request, encode_open_request, flags, EchoReply, EchoRequest, OpenReply,
|
|
||||||
OpenRequest, Params, ServerFill, TimestampFields, PROTOCOL_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{
|
config::{ClientConfig, RecvBudget},
|
||||||
ClientConfig, RecvBudget, RunMode, MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES,
|
|
||||||
MAX_UDP_PAYLOAD_LENGTH,
|
|
||||||
},
|
|
||||||
error::ClientError,
|
error::ClientError,
|
||||||
event::{
|
event::{ClientEvent, OpenOutcome},
|
||||||
ClientEvent, OneWayDelaySample, OpenOutcome, ReceivedStatsSample, RttSample, ServerTiming,
|
|
||||||
SignedDuration, WarningKind,
|
|
||||||
},
|
|
||||||
metadata::ReceiveMeta,
|
|
||||||
probe::{CompletedSet, PendingMap, PendingProbe, TimedOutMap},
|
|
||||||
receive::recv_datagram,
|
receive::recv_datagram,
|
||||||
session::{negotiate_params, ActiveSession, ClientPhase, CloseSource, NegotiatedParams},
|
runtime::{recv_buffer_size, SendProbeResult, SessionRuntime, MAX_OPEN_PACKET_SIZE},
|
||||||
socket::{connect_udp_socket, resolve_remote, validate_open_timeouts},
|
socket::{connect_udp_socket, resolve_remote, validate_open_timeouts},
|
||||||
socket_options::{apply_dscp_to_socket, clear_dscp_on_socket},
|
socket_options::{apply_dscp_to_socket, clear_dscp_on_socket},
|
||||||
timing::ClientTimestamp,
|
timing::ClientTimestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_OPEN_PACKET_SIZE: usize = 512;
|
#[cfg(test)]
|
||||||
const MIN_RECV_BUFFER_SIZE: usize = 2048;
|
use crate::{
|
||||||
|
runtime::{
|
||||||
|
compute_one_way, compute_rtt, params_from_config, sequence_is_after, sequence_is_before,
|
||||||
|
unix_epoch_ns_i64, update_highest_received,
|
||||||
|
},
|
||||||
|
session::negotiate_params,
|
||||||
|
NegotiatedParams, RunMode, SignedDuration, WarningKind, MAX_UDP_PAYLOAD_LENGTH,
|
||||||
|
};
|
||||||
|
|
||||||
/// Low-level synchronous IRTT client.
|
/// Low-level synchronous IRTT client.
|
||||||
///
|
///
|
||||||
@@ -40,13 +36,9 @@ const MIN_RECV_BUFFER_SIZE: usize = 2048;
|
|||||||
/// instead.
|
/// instead.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
config: ClientConfig,
|
runtime: SessionRuntime,
|
||||||
socket: UdpSocket,
|
socket: UdpSocket,
|
||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
requested: Params,
|
|
||||||
negotiated: Option<NegotiatedParams>,
|
|
||||||
phase: ClientPhase,
|
|
||||||
session: Option<ActiveSession>,
|
|
||||||
recv_buffer: Vec<u8>,
|
recv_buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,29 +50,15 @@ impl Client {
|
|||||||
/// IRTT open exchange.
|
/// IRTT open exchange.
|
||||||
pub fn connect(config: ClientConfig) -> Result<Self, ClientError> {
|
pub fn connect(config: ClientConfig) -> Result<Self, ClientError> {
|
||||||
validate_open_timeouts(&config.open_timeouts)?;
|
validate_open_timeouts(&config.open_timeouts)?;
|
||||||
if config.max_pending_probes == 0 {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: "max_pending_probes must be greater than zero".to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if config.probe_timeout == Duration::ZERO {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: "probe_timeout must be greater than zero".to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let remote = resolve_remote(&config)?;
|
let remote = resolve_remote(&config)?;
|
||||||
|
let runtime = SessionRuntime::new(config.clone(), remote)?;
|
||||||
let socket = connect_udp_socket(&config.socket_config, remote)?;
|
let socket = connect_udp_socket(&config.socket_config, remote)?;
|
||||||
let requested = params_from_config(&config)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
runtime,
|
||||||
socket,
|
socket,
|
||||||
remote,
|
remote,
|
||||||
requested,
|
recv_buffer: vec![0_u8; recv_buffer_size(false, None)?],
|
||||||
negotiated: None,
|
|
||||||
phase: ClientPhase::Connected,
|
|
||||||
session: None,
|
|
||||||
recv_buffer: vec![0_u8; MIN_RECV_BUFFER_SIZE],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,34 +68,37 @@ impl Client {
|
|||||||
/// client into either an open probe session or completed no-test state.
|
/// client into either an open probe session or completed no-test state.
|
||||||
/// Open attempts use [`ClientConfig::open_timeouts`].
|
/// Open attempts use [`ClientConfig::open_timeouts`].
|
||||||
pub fn open(&mut self) -> Result<OpenOutcome, ClientError> {
|
pub fn open(&mut self) -> Result<OpenOutcome, ClientError> {
|
||||||
match self.phase {
|
|
||||||
ClientPhase::Connected => {}
|
|
||||||
ClientPhase::Open { .. } => return Err(ClientError::AlreadyOpen),
|
|
||||||
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
|
||||||
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
|
||||||
}
|
|
||||||
|
|
||||||
let outcome = (|| -> Result<OpenOutcome, ClientError> {
|
let outcome = (|| -> Result<OpenOutcome, ClientError> {
|
||||||
let request = OpenRequest {
|
let packet = self.runtime.open_packet()?;
|
||||||
params: self.requested.clone(),
|
|
||||||
close: self.config.run_mode == RunMode::NoTest,
|
|
||||||
};
|
|
||||||
|
|
||||||
let packet = encode_open_request(&request, self.config.hmac_key.as_deref())?;
|
|
||||||
let mut buf = [0_u8; MAX_OPEN_PACKET_SIZE];
|
let mut buf = [0_u8; MAX_OPEN_PACKET_SIZE];
|
||||||
|
|
||||||
for timeout in &self.config.open_timeouts {
|
for timeout in &self.runtime.config().open_timeouts {
|
||||||
self.socket.set_read_timeout(Some(*timeout))?;
|
self.socket.set_read_timeout(Some(*timeout))?;
|
||||||
self.socket.send(&packet)?;
|
self.socket.send(&packet)?;
|
||||||
|
|
||||||
match self.socket.recv(&mut buf) {
|
match self.socket.recv(&mut buf) {
|
||||||
Ok(size) => {
|
Ok(size) => {
|
||||||
let reply = irtt_proto::decode_open_reply(
|
let reply = self.runtime.decode_open_reply(&buf[..size])?;
|
||||||
&buf[..size],
|
let remote = self.remote;
|
||||||
self.config.hmac_key.as_deref(),
|
let has_hmac = self.runtime.has_hmac();
|
||||||
)?;
|
let socket = &self.socket;
|
||||||
|
let recv_buffer = &mut self.recv_buffer;
|
||||||
|
|
||||||
return self.accept_open_reply(reply, ClientTimestamp::now());
|
return self.runtime.accept_open_reply(
|
||||||
|
reply,
|
||||||
|
ClientTimestamp::now(),
|
||||||
|
|negotiated| {
|
||||||
|
let size = recv_buffer_size(has_hmac, Some(negotiated))?;
|
||||||
|
let negotiated_dscp = u8::try_from(negotiated.params.dscp)
|
||||||
|
.map_err(|_| ClientError::InvalidConfig {
|
||||||
|
reason: "negotiated dscp must be in range 0..=63"
|
||||||
|
.to_owned(),
|
||||||
|
})?;
|
||||||
|
apply_dscp_to_socket(socket, remote, negotiated_dscp)?;
|
||||||
|
recv_buffer.resize(size, 0);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(err)
|
Err(err)
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -133,7 +114,7 @@ impl Client {
|
|||||||
|
|
||||||
let restore = self
|
let restore = self
|
||||||
.socket
|
.socket
|
||||||
.set_read_timeout(self.config.socket_config.recv_timeout);
|
.set_read_timeout(self.runtime.config().socket_config.recv_timeout);
|
||||||
|
|
||||||
match (outcome, restore) {
|
match (outcome, restore) {
|
||||||
(Ok(outcome), Ok(())) => Ok(outcome),
|
(Ok(outcome), Ok(())) => Ok(outcome),
|
||||||
@@ -148,46 +129,24 @@ impl Client {
|
|||||||
/// The close event means the client has sent its close packet and stopped
|
/// The close event means the client has sent its close packet and stopped
|
||||||
/// tracking the session locally; it is not a server acknowledgement.
|
/// tracking the session locally; it is not a server acknowledgement.
|
||||||
pub fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
pub fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
let token = match self.phase {
|
let socket = &self.socket;
|
||||||
ClientPhase::Open { token } => token,
|
let remote = self.remote;
|
||||||
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
self.runtime.close_with(|packet| {
|
||||||
ClientPhase::Connected | ClientPhase::NoTestCompleted => {
|
clear_dscp_on_socket(socket, remote)?;
|
||||||
return Err(ClientError::NotOpen)
|
socket.send(packet)?;
|
||||||
}
|
Ok(())
|
||||||
};
|
})
|
||||||
|
|
||||||
clear_dscp_on_socket(&self.socket, self.remote)?;
|
|
||||||
let packet =
|
|
||||||
encode_close_request(&CloseRequest { token }, self.config.hmac_key.as_deref())?;
|
|
||||||
self.socket.send(&packet)?;
|
|
||||||
self.phase = ClientPhase::Closed {
|
|
||||||
source: CloseSource::Local,
|
|
||||||
};
|
|
||||||
if let Some(session) = self.session.as_mut() {
|
|
||||||
session.timed_out.clear();
|
|
||||||
}
|
|
||||||
self.session = None;
|
|
||||||
|
|
||||||
Ok(vec![ClientEvent::SessionClosed {
|
|
||||||
remote: self.remote,
|
|
||||||
token,
|
|
||||||
at: ClientTimestamp::now(),
|
|
||||||
}])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the monotonic deadline for the next probe send, if another probe
|
/// Return the monotonic deadline for the next probe send, if another probe
|
||||||
/// is scheduled.
|
/// is scheduled.
|
||||||
pub fn next_send_deadline(&self) -> Option<Instant> {
|
pub fn next_send_deadline(&self) -> Option<Instant> {
|
||||||
let session = self.session.as_ref()?;
|
self.runtime.next_send_deadline()
|
||||||
if session.sending_done {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(session.next_send_at)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the local timeout used to classify pending probes as lost.
|
/// Return the local timeout used to classify pending probes as lost.
|
||||||
pub fn probe_timeout(&self) -> Duration {
|
pub fn probe_timeout(&self) -> Duration {
|
||||||
self.config.probe_timeout
|
self.runtime.probe_timeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send one echo probe if the negotiated run is still active.
|
/// Send one echo probe if the negotiated run is still active.
|
||||||
@@ -204,126 +163,6 @@ impl Client {
|
|||||||
/// Returns an empty event list when the socket read would block or times
|
/// Returns an empty event list when the socket read would block or times
|
||||||
/// out. Malformed or unrelated datagrams are reported as warning events.
|
/// out. Malformed or unrelated datagrams are reported as warning events.
|
||||||
pub fn recv_once(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
pub fn recv_once(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
self.recv_once_inner(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_probe_inner(
|
|
||||||
&mut self,
|
|
||||||
override_ts: Option<ClientTimestamp>,
|
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
|
||||||
let token = match self.phase {
|
|
||||||
ClientPhase::Open { token } => token,
|
|
||||||
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
|
||||||
ClientPhase::Connected => return Err(ClientError::NotOpen),
|
|
||||||
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
|
||||||
};
|
|
||||||
|
|
||||||
let session = self
|
|
||||||
.session
|
|
||||||
.as_mut()
|
|
||||||
.expect("session must exist when phase is Open");
|
|
||||||
|
|
||||||
if session.sending_done {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = override_ts.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
|
|
||||||
if let Some(end) = session.end_mono {
|
|
||||||
if now.mono >= end {
|
|
||||||
session.sending_done = true;
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.pending.check_capacity()?;
|
|
||||||
|
|
||||||
let negotiated = self
|
|
||||||
.negotiated
|
|
||||||
.as_ref()
|
|
||||||
.expect("negotiated must exist when Open");
|
|
||||||
|
|
||||||
let wire_seq = session.next_wire_seq;
|
|
||||||
let scheduled_at = session.next_send_at;
|
|
||||||
|
|
||||||
let request = EchoRequest {
|
|
||||||
token,
|
|
||||||
sequence: wire_seq,
|
|
||||||
params: negotiated.params.clone(),
|
|
||||||
payload: vec![],
|
|
||||||
};
|
|
||||||
let packet = encode_echo_request(&request, self.config.hmac_key.as_deref())?;
|
|
||||||
let sent_at = override_ts.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
let send_call_start = Instant::now();
|
|
||||||
let bytes = self.socket.send(&packet)?;
|
|
||||||
let send_call = send_call_start.elapsed();
|
|
||||||
let timer_error = instant_abs_diff(sent_at.mono, scheduled_at);
|
|
||||||
|
|
||||||
let session = self
|
|
||||||
.session
|
|
||||||
.as_mut()
|
|
||||||
.expect("session must exist when phase is Open");
|
|
||||||
|
|
||||||
let pending = PendingProbe {
|
|
||||||
wire_seq,
|
|
||||||
sent_at,
|
|
||||||
timeout_at: sent_at
|
|
||||||
.mono
|
|
||||||
.checked_add(self.config.probe_timeout)
|
|
||||||
.ok_or(ClientError::DurationOverflow)?,
|
|
||||||
};
|
|
||||||
session.pending.insert(pending)?;
|
|
||||||
|
|
||||||
session.next_wire_seq = session.next_wire_seq.wrapping_add(1);
|
|
||||||
session.packets_sent =
|
|
||||||
session
|
|
||||||
.packets_sent
|
|
||||||
.checked_add(1)
|
|
||||||
.ok_or(ClientError::CounterOverflow {
|
|
||||||
counter: "packets_sent",
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let negotiated = self
|
|
||||||
.negotiated
|
|
||||||
.as_ref()
|
|
||||||
.expect("negotiated must exist when Open");
|
|
||||||
let interval_ns = u64::try_from(negotiated.params.interval_ns)
|
|
||||||
.expect("validated positive negotiated interval");
|
|
||||||
let session = self
|
|
||||||
.session
|
|
||||||
.as_mut()
|
|
||||||
.expect("session must exist when phase is Open");
|
|
||||||
session.next_send_at =
|
|
||||||
next_probe_deadline(session.start_mono, interval_ns, session.packets_sent)?;
|
|
||||||
|
|
||||||
if let Some(end) = session.end_mono {
|
|
||||||
if session.next_send_at >= end {
|
|
||||||
session.sending_done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(vec![ClientEvent::EchoSent {
|
|
||||||
seq: wire_seq,
|
|
||||||
remote: self.remote,
|
|
||||||
scheduled_at,
|
|
||||||
sent_at,
|
|
||||||
bytes,
|
|
||||||
send_call,
|
|
||||||
timer_error,
|
|
||||||
}])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recv_once_inner(
|
|
||||||
&mut self,
|
|
||||||
override_ts: Option<ClientTimestamp>,
|
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
|
||||||
match self.phase {
|
|
||||||
ClientPhase::Open { .. } => {}
|
|
||||||
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
|
||||||
ClientPhase::Connected => return Err(ClientError::NotOpen),
|
|
||||||
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
|
||||||
}
|
|
||||||
|
|
||||||
let datagram = match recv_datagram(&self.socket, &mut self.recv_buffer) {
|
let datagram = match recv_datagram(&self.socket, &mut self.recv_buffer) {
|
||||||
Ok(datagram) => datagram,
|
Ok(datagram) => datagram,
|
||||||
Err(err)
|
Err(err)
|
||||||
@@ -337,16 +176,11 @@ impl Client {
|
|||||||
Err(err) => return Err(ClientError::Socket(err)),
|
Err(err) => return Err(ClientError::Socket(err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let now = override_ts.unwrap_or(datagram.received_at);
|
self.runtime.process_received_echo_packet(
|
||||||
let packet_len = datagram.len;
|
&self.recv_buffer[..datagram.len],
|
||||||
let Some(reply) = self.decode_received_packet(&self.recv_buffer[..packet_len]) else {
|
datagram.received_at,
|
||||||
return Ok(vec![ClientEvent::Warning {
|
datagram.meta,
|
||||||
kind: WarningKind::MalformedOrUnrelatedPacket,
|
)
|
||||||
message: "dropped malformed or unrelated packet".to_owned(),
|
|
||||||
at: now,
|
|
||||||
}]);
|
|
||||||
};
|
|
||||||
self.process_echo_reply(reply, packet_len, now, datagram.meta)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receive and classify datagrams until a receive produces no events or the
|
/// Receive and classify datagrams until a receive produces no events or the
|
||||||
@@ -379,30 +213,7 @@ impl Client {
|
|||||||
///
|
///
|
||||||
/// `now` is monotonic time only; wall-clock time is not used for timeout expiry.
|
/// `now` is monotonic time only; wall-clock time is not used for timeout expiry.
|
||||||
pub fn poll_timeouts_at(&mut self, now: Instant) -> Result<Vec<ClientEvent>, ClientError> {
|
pub fn poll_timeouts_at(&mut self, now: Instant) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
match self.phase {
|
self.runtime.poll_timeouts_at(now)
|
||||||
ClientPhase::Open { .. } => {}
|
|
||||||
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
|
||||||
ClientPhase::Connected => return Err(ClientError::NotOpen),
|
|
||||||
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
|
||||||
}
|
|
||||||
|
|
||||||
let session = self
|
|
||||||
.session
|
|
||||||
.as_mut()
|
|
||||||
.expect("session must exist when phase is Open");
|
|
||||||
|
|
||||||
let expired = session.pending.drain_expired(now);
|
|
||||||
let mut events = Vec::with_capacity(expired.len());
|
|
||||||
for probe in expired {
|
|
||||||
events.push(ClientEvent::EchoLoss {
|
|
||||||
seq: probe.wire_seq,
|
|
||||||
sent_at: probe.sent_at,
|
|
||||||
timeout_at: probe.timeout_at,
|
|
||||||
});
|
|
||||||
session.timed_out.insert(probe);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(events)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether the current run has completed.
|
/// Return whether the current run has completed.
|
||||||
@@ -411,13 +222,7 @@ impl Client {
|
|||||||
/// pending probes have either replied or timed out. No-test and closed
|
/// pending probes have either replied or timed out. No-test and closed
|
||||||
/// sessions are also considered complete.
|
/// sessions are also considered complete.
|
||||||
pub fn is_run_complete(&self) -> bool {
|
pub fn is_run_complete(&self) -> bool {
|
||||||
let Some(session) = self.session.as_ref() else {
|
self.runtime.is_run_complete()
|
||||||
return matches!(
|
|
||||||
self.phase,
|
|
||||||
ClientPhase::Closed { .. } | ClientPhase::NoTestCompleted
|
|
||||||
);
|
|
||||||
};
|
|
||||||
session.sending_done && session.pending.len() == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether the session was closed by a peer close-flagged reply.
|
/// Return whether the session was closed by a peer close-flagged reply.
|
||||||
@@ -427,295 +232,32 @@ impl Client {
|
|||||||
/// avoid treating a successfully observed peer close as a local cleanup
|
/// avoid treating a successfully observed peer close as a local cleanup
|
||||||
/// failure.
|
/// failure.
|
||||||
pub fn is_peer_closed(&self) -> bool {
|
pub fn is_peer_closed(&self) -> bool {
|
||||||
matches!(
|
self.runtime.is_peer_closed()
|
||||||
self.phase,
|
|
||||||
ClientPhase::Closed {
|
|
||||||
source: CloseSource::Peer
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_timed_out_metadata(&self) -> bool {
|
pub(crate) fn has_timed_out_metadata(&self) -> bool {
|
||||||
self.session
|
self.runtime.has_timed_out_metadata()
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|session| session.timed_out.len() > 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn packets_sent(&self) -> u64 {
|
pub(crate) fn packets_sent(&self) -> u64 {
|
||||||
self.session
|
self.runtime.packets_sent()
|
||||||
.as_ref()
|
|
||||||
.map_or(0, |session| session.packets_sent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_received_packet(&self, packet: &[u8]) -> Option<EchoReply> {
|
fn send_probe_inner(
|
||||||
let negotiated = self
|
|
||||||
.negotiated
|
|
||||||
.as_ref()
|
|
||||||
.expect("negotiated must exist when Open");
|
|
||||||
|
|
||||||
decode_echo_reply(packet, &negotiated.params, self.config.hmac_key.as_deref()).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_echo_reply(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
reply: EchoReply,
|
override_ts: Option<ClientTimestamp>,
|
||||||
packet_len: usize,
|
|
||||||
now: ClientTimestamp,
|
|
||||||
meta: ReceiveMeta,
|
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
let token = match self.phase {
|
let socket = &self.socket;
|
||||||
ClientPhase::Open { token } => token,
|
self.runtime.send_probe_with(override_ts, |packet| {
|
||||||
_ => unreachable!(),
|
let sent_at = override_ts.unwrap_or_else(ClientTimestamp::now);
|
||||||
};
|
let send_call_start = Instant::now();
|
||||||
if reply.token != token {
|
let bytes = socket.send(packet)?;
|
||||||
return Ok(vec![ClientEvent::Warning {
|
let send_call = send_call_start.elapsed();
|
||||||
kind: WarningKind::WrongToken,
|
Ok(SendProbeResult {
|
||||||
message: format!(
|
sent_at,
|
||||||
"dropped reply with wrong token: expected {token:#x}, got {:#x}",
|
bytes,
|
||||||
reply.token
|
send_call,
|
||||||
),
|
|
||||||
at: now,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let session = self.session.as_mut().expect("session must exist when Open");
|
|
||||||
|
|
||||||
let wire_seq = reply.sequence;
|
|
||||||
|
|
||||||
if let Some(pending) = session.pending.remove(wire_seq) {
|
|
||||||
let rtt = compute_rtt(&pending.sent_at, &now, &reply.timestamps);
|
|
||||||
let server_timing = build_server_timing(&reply.timestamps);
|
|
||||||
let one_way = compute_one_way(&pending.sent_at, &now, &reply.timestamps);
|
|
||||||
let received_stats = build_received_stats(&reply);
|
|
||||||
let is_late = session
|
|
||||||
.highest_received_seq
|
|
||||||
.is_some_and(|h| sequence_is_before(wire_seq, h));
|
|
||||||
let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
|
|
||||||
|
|
||||||
update_highest_received(&mut session.highest_received_seq, wire_seq);
|
|
||||||
session.completed.insert(wire_seq);
|
|
||||||
|
|
||||||
let mut events = Vec::new();
|
|
||||||
if is_late {
|
|
||||||
events.push(ClientEvent::LateReply {
|
|
||||||
seq: wire_seq,
|
|
||||||
highest_seen,
|
|
||||||
remote: self.remote,
|
|
||||||
sent_at: Some(pending.sent_at),
|
|
||||||
received_at: now,
|
|
||||||
rtt: Some(rtt),
|
|
||||||
server_timing,
|
|
||||||
one_way,
|
|
||||||
received_stats,
|
|
||||||
bytes: packet_len,
|
|
||||||
packet_meta: meta.into(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
events.push(ClientEvent::EchoReply {
|
|
||||||
seq: wire_seq,
|
|
||||||
remote: self.remote,
|
|
||||||
sent_at: pending.sent_at,
|
|
||||||
received_at: now,
|
|
||||||
rtt,
|
|
||||||
server_timing,
|
|
||||||
one_way,
|
|
||||||
received_stats,
|
|
||||||
bytes: packet_len,
|
|
||||||
packet_meta: meta.into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if flags::has(reply.flags, flags::FLAG_CLOSE) {
|
|
||||||
self.close_from_peer(token, now, &mut events)?;
|
|
||||||
}
|
|
||||||
Ok(events)
|
|
||||||
} else if session.completed.contains(wire_seq) {
|
|
||||||
update_highest_received(&mut session.highest_received_seq, wire_seq);
|
|
||||||
Ok(vec![ClientEvent::DuplicateReply {
|
|
||||||
seq: wire_seq,
|
|
||||||
remote: self.remote,
|
|
||||||
received_at: now,
|
|
||||||
bytes: packet_len,
|
|
||||||
}])
|
|
||||||
} else if let Some(timed_out) = session.timed_out.remove(wire_seq) {
|
|
||||||
let rtt = compute_rtt(&timed_out.sent_at, &now, &reply.timestamps);
|
|
||||||
let server_timing = build_server_timing(&reply.timestamps);
|
|
||||||
let one_way = compute_one_way(&timed_out.sent_at, &now, &reply.timestamps);
|
|
||||||
let received_stats = build_received_stats(&reply);
|
|
||||||
let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
|
|
||||||
update_highest_received(&mut session.highest_received_seq, wire_seq);
|
|
||||||
session.completed.insert(wire_seq);
|
|
||||||
|
|
||||||
let mut events = vec![ClientEvent::LateReply {
|
|
||||||
seq: wire_seq,
|
|
||||||
highest_seen,
|
|
||||||
remote: self.remote,
|
|
||||||
sent_at: Some(timed_out.sent_at),
|
|
||||||
received_at: now,
|
|
||||||
rtt: Some(rtt),
|
|
||||||
server_timing,
|
|
||||||
one_way,
|
|
||||||
received_stats,
|
|
||||||
bytes: packet_len,
|
|
||||||
packet_meta: meta.into(),
|
|
||||||
}];
|
|
||||||
if flags::has(reply.flags, flags::FLAG_CLOSE) {
|
|
||||||
self.close_from_peer(token, now, &mut events)?;
|
|
||||||
}
|
|
||||||
Ok(events)
|
|
||||||
} else if session
|
|
||||||
.highest_received_seq
|
|
||||||
.is_some_and(|h| sequence_is_before(wire_seq, h))
|
|
||||||
{
|
|
||||||
Ok(vec![ClientEvent::LateReply {
|
|
||||||
seq: wire_seq,
|
|
||||||
highest_seen: session.highest_received_seq.unwrap(),
|
|
||||||
remote: self.remote,
|
|
||||||
sent_at: None,
|
|
||||||
received_at: now,
|
|
||||||
rtt: None,
|
|
||||||
server_timing: build_server_timing(&reply.timestamps),
|
|
||||||
one_way: None,
|
|
||||||
received_stats: build_received_stats(&reply),
|
|
||||||
bytes: packet_len,
|
|
||||||
packet_meta: meta.into(),
|
|
||||||
}])
|
|
||||||
} else {
|
|
||||||
Ok(vec![ClientEvent::Warning {
|
|
||||||
kind: WarningKind::UntrackedReply,
|
|
||||||
message: format!(
|
|
||||||
"dropped reply with untracked seq {wire_seq} (no pending or completed entry)"
|
|
||||||
),
|
|
||||||
at: now,
|
|
||||||
}])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_from_peer(
|
|
||||||
&mut self,
|
|
||||||
token: u64,
|
|
||||||
now: ClientTimestamp,
|
|
||||||
events: &mut Vec<ClientEvent>,
|
|
||||||
) -> Result<(), ClientError> {
|
|
||||||
clear_dscp_on_socket(&self.socket, self.remote)?;
|
|
||||||
self.phase = ClientPhase::Closed {
|
|
||||||
source: CloseSource::Peer,
|
|
||||||
};
|
|
||||||
if let Some(session) = self.session.as_mut() {
|
|
||||||
session.timed_out.clear();
|
|
||||||
}
|
|
||||||
self.session = None;
|
|
||||||
events.push(ClientEvent::SessionClosed {
|
|
||||||
remote: self.remote,
|
|
||||||
token,
|
|
||||||
at: now,
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accept_open_reply(
|
|
||||||
&mut self,
|
|
||||||
reply: OpenReply,
|
|
||||||
now: ClientTimestamp,
|
|
||||||
) -> Result<OpenOutcome, ClientError> {
|
|
||||||
if reply.params.protocol_version != PROTOCOL_VERSION {
|
|
||||||
return Err(ClientError::ProtocolVersionMismatch {
|
|
||||||
requested: PROTOCOL_VERSION,
|
|
||||||
received: reply.params.protocol_version,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let reply_is_close = flags::has(reply.flags, flags::FLAG_CLOSE);
|
|
||||||
match self.config.run_mode {
|
|
||||||
RunMode::Normal if reply_is_close => Err(ClientError::ServerRejected),
|
|
||||||
RunMode::Normal if reply.token == 0 => Err(ClientError::ZeroToken),
|
|
||||||
RunMode::Normal => self.accept_normal_open(reply, now),
|
|
||||||
RunMode::NoTest if !reply_is_close => Err(ClientError::UnexpectedNoTestReply),
|
|
||||||
RunMode::NoTest if reply.token != 0 => {
|
|
||||||
Err(ClientError::NonZeroNoTestToken { token: reply.token })
|
|
||||||
}
|
|
||||||
RunMode::NoTest => self.accept_no_test_open(reply, now),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accept_normal_open(
|
|
||||||
&mut self,
|
|
||||||
reply: OpenReply,
|
|
||||||
now: ClientTimestamp,
|
|
||||||
) -> Result<OpenOutcome, ClientError> {
|
|
||||||
let token = reply.token;
|
|
||||||
let negotiated = negotiate_params(
|
|
||||||
&self.requested,
|
|
||||||
reply.params,
|
|
||||||
self.config.negotiation_policy,
|
|
||||||
)?;
|
|
||||||
let recv_buffer_size = recv_buffer_size(self.config.hmac_key.is_some(), Some(&negotiated))?;
|
|
||||||
let negotiated_dscp =
|
|
||||||
u8::try_from(negotiated.params.dscp).map_err(|_| ClientError::InvalidConfig {
|
|
||||||
reason: "negotiated dscp must be in range 0..=63".to_owned(),
|
|
||||||
})?;
|
|
||||||
apply_dscp_to_socket(&self.socket, self.remote, negotiated_dscp)?;
|
|
||||||
self.negotiated = Some(negotiated.clone());
|
|
||||||
self.recv_buffer.resize(recv_buffer_size, 0);
|
|
||||||
self.phase = ClientPhase::Open { token };
|
|
||||||
|
|
||||||
let end_mono = if negotiated.params.duration_ns > 0 {
|
|
||||||
Some(negotiated_end_mono(
|
|
||||||
now.mono,
|
|
||||||
negotiated.params.duration_ns,
|
|
||||||
)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
self.session = Some(ActiveSession {
|
|
||||||
next_wire_seq: 0,
|
|
||||||
highest_received_seq: None,
|
|
||||||
packets_sent: 0,
|
|
||||||
start_mono: now.mono,
|
|
||||||
end_mono,
|
|
||||||
next_send_at: now.mono,
|
|
||||||
pending: PendingMap::new(self.config.max_pending_probes),
|
|
||||||
timed_out: TimedOutMap::new(self.config.max_pending_probes),
|
|
||||||
completed: CompletedSet::new(self.config.max_pending_probes),
|
|
||||||
sending_done: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let event = ClientEvent::SessionStarted {
|
|
||||||
remote: self.remote,
|
|
||||||
token,
|
|
||||||
negotiated: negotiated.clone(),
|
|
||||||
at: now,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(OpenOutcome::Started {
|
|
||||||
remote: self.remote,
|
|
||||||
token,
|
|
||||||
negotiated,
|
|
||||||
event,
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
fn accept_no_test_open(
|
|
||||||
&mut self,
|
|
||||||
reply: OpenReply,
|
|
||||||
now: ClientTimestamp,
|
|
||||||
) -> Result<OpenOutcome, ClientError> {
|
|
||||||
let negotiated = negotiate_params(
|
|
||||||
&self.requested,
|
|
||||||
reply.params,
|
|
||||||
self.config.negotiation_policy,
|
|
||||||
)?;
|
|
||||||
self.negotiated = Some(negotiated.clone());
|
|
||||||
self.phase = ClientPhase::NoTestCompleted;
|
|
||||||
let event = ClientEvent::NoTestCompleted {
|
|
||||||
remote: self.remote,
|
|
||||||
negotiated: negotiated.clone(),
|
|
||||||
at: now,
|
|
||||||
};
|
|
||||||
Ok(OpenOutcome::NoTestCompleted {
|
|
||||||
remote: self.remote,
|
|
||||||
negotiated,
|
|
||||||
event,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,233 +269,5 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_highest_received(highest_received_seq: &mut Option<u32>, wire_seq: u32) {
|
|
||||||
*highest_received_seq = Some(highest_received_seq.map_or(wire_seq, |h| {
|
|
||||||
if sequence_is_after(wire_seq, h) {
|
|
||||||
wire_seq
|
|
||||||
} else {
|
|
||||||
h
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_probe_deadline(
|
|
||||||
start: Instant,
|
|
||||||
interval_ns: u64,
|
|
||||||
packets_sent: u64,
|
|
||||||
) -> Result<Instant, ClientError> {
|
|
||||||
let offset_ns = interval_ns
|
|
||||||
.checked_mul(packets_sent)
|
|
||||||
.ok_or(ClientError::DurationOverflow)?;
|
|
||||||
start
|
|
||||||
.checked_add(Duration::from_nanos(offset_ns))
|
|
||||||
.ok_or(ClientError::DurationOverflow)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sequence_is_after(candidate: u32, current: u32) -> bool {
|
|
||||||
candidate != current && candidate.wrapping_sub(current) < (1 << 31)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sequence_is_before(candidate: u32, current: u32) -> bool {
|
|
||||||
current != candidate && current.wrapping_sub(candidate) < (1 << 31)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn instant_abs_diff(left: Instant, right: Instant) -> Duration {
|
|
||||||
left.checked_duration_since(right)
|
|
||||||
.or_else(|| right.checked_duration_since(left))
|
|
||||||
.unwrap_or(Duration::ZERO)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn recv_buffer_size(
|
|
||||||
has_hmac: bool,
|
|
||||||
negotiated: Option<&NegotiatedParams>,
|
|
||||||
) -> Result<usize, ClientError> {
|
|
||||||
Ok(match negotiated {
|
|
||||||
Some(negotiated) => echo_packet_len(has_hmac, &negotiated.params)?
|
|
||||||
.saturating_add(1)
|
|
||||||
.max(MIN_RECV_BUFFER_SIZE),
|
|
||||||
None => MIN_RECV_BUFFER_SIZE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_rtt(
|
|
||||||
sent_at: &ClientTimestamp,
|
|
||||||
received_at: &ClientTimestamp,
|
|
||||||
ts: &TimestampFields,
|
|
||||||
) -> RttSample {
|
|
||||||
let raw = received_at
|
|
||||||
.mono
|
|
||||||
.checked_duration_since(sent_at.mono)
|
|
||||||
.unwrap_or(Duration::ZERO);
|
|
||||||
|
|
||||||
let server_processing = compute_server_processing(ts);
|
|
||||||
|
|
||||||
let adjusted = server_processing
|
|
||||||
.map(|sp| SignedDuration::from_nanos(duration_ns_i128(raw) - duration_ns_i128(sp)));
|
|
||||||
let effective = adjusted.unwrap_or_else(|| SignedDuration::from_duration(raw));
|
|
||||||
|
|
||||||
RttSample {
|
|
||||||
raw,
|
|
||||||
adjusted,
|
|
||||||
effective,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn duration_ns_i128(duration: Duration) -> i128 {
|
|
||||||
i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_server_processing(ts: &TimestampFields) -> Option<Duration> {
|
|
||||||
if let (Some(recv_mono), Some(send_mono)) = (ts.recv_mono, ts.send_mono) {
|
|
||||||
let diff = send_mono.checked_sub(recv_mono)?;
|
|
||||||
return Some(Duration::from_nanos(u64::try_from(diff).ok()?));
|
|
||||||
}
|
|
||||||
if let (Some(recv_wall), Some(send_wall)) = (ts.recv_wall, ts.send_wall) {
|
|
||||||
let diff = send_wall.checked_sub(recv_wall)?;
|
|
||||||
return Some(Duration::from_nanos(u64::try_from(diff).ok()?));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_server_timing(ts: &TimestampFields) -> Option<ServerTiming> {
|
|
||||||
if ts.recv_wall.is_none()
|
|
||||||
&& ts.recv_mono.is_none()
|
|
||||||
&& ts.send_wall.is_none()
|
|
||||||
&& ts.send_mono.is_none()
|
|
||||||
&& ts.midpoint_wall.is_none()
|
|
||||||
&& ts.midpoint_mono.is_none()
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(ServerTiming {
|
|
||||||
receive_wall_ns: ts.recv_wall,
|
|
||||||
receive_mono_ns: ts.recv_mono,
|
|
||||||
send_wall_ns: ts.send_wall,
|
|
||||||
send_mono_ns: ts.send_mono,
|
|
||||||
midpoint_wall_ns: ts.midpoint_wall,
|
|
||||||
midpoint_mono_ns: ts.midpoint_mono,
|
|
||||||
processing: compute_server_processing(ts),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_one_way(
|
|
||||||
sent_at: &ClientTimestamp,
|
|
||||||
received_at: &ClientTimestamp,
|
|
||||||
ts: &TimestampFields,
|
|
||||||
) -> Option<OneWayDelaySample> {
|
|
||||||
let server_recv_wall = ts.recv_wall.or(ts.midpoint_wall);
|
|
||||||
let server_send_wall = ts.send_wall.or(ts.midpoint_wall);
|
|
||||||
|
|
||||||
let client_send_ns = unix_epoch_ns_i64(sent_at.wall);
|
|
||||||
let client_recv_ns = unix_epoch_ns_i64(received_at.wall);
|
|
||||||
|
|
||||||
let c2s = server_recv_wall
|
|
||||||
.zip(client_send_ns)
|
|
||||||
.and_then(|(srv, cli)| srv.checked_sub(cli))
|
|
||||||
.map(|d| SignedDuration::from_nanos(i128::from(d)));
|
|
||||||
let s2c = client_recv_ns
|
|
||||||
.zip(server_send_wall)
|
|
||||||
.and_then(|(cli, srv)| cli.checked_sub(srv))
|
|
||||||
.map(|d| SignedDuration::from_nanos(i128::from(d)));
|
|
||||||
|
|
||||||
if c2s.is_none() && s2c.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(OneWayDelaySample {
|
|
||||||
client_to_server: c2s,
|
|
||||||
server_to_client: s2c,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unix_epoch_ns_i64(time: SystemTime) -> Option<i64> {
|
|
||||||
time.duration_since(UNIX_EPOCH)
|
|
||||||
.ok()
|
|
||||||
.and_then(|duration| i64::try_from(duration.as_nanos()).ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_received_stats(reply: &EchoReply) -> Option<ReceivedStatsSample> {
|
|
||||||
if reply.recv_count.is_none() && reply.recv_window.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(ReceivedStatsSample {
|
|
||||||
count: reply.recv_count,
|
|
||||||
window: reply.recv_window,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn params_from_config(config: &ClientConfig) -> Result<Params, ClientError> {
|
|
||||||
validate_protocol_config(config)?;
|
|
||||||
Ok(Params {
|
|
||||||
protocol_version: PROTOCOL_VERSION,
|
|
||||||
duration_ns: match config.duration {
|
|
||||||
Some(duration) => config_duration_to_ns("duration", duration)?,
|
|
||||||
None => 0,
|
|
||||||
},
|
|
||||||
interval_ns: config_duration_to_ns("interval", config.interval)?,
|
|
||||||
length: i64::from(config.length),
|
|
||||||
received_stats: config.received_stats,
|
|
||||||
stamp_at: config.stamp_at,
|
|
||||||
clock: config.clock,
|
|
||||||
dscp: i64::from(config.dscp),
|
|
||||||
server_fill: config.server_fill.clone().map(|value| ServerFill { value }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_protocol_config(config: &ClientConfig) -> Result<(), ClientError> {
|
|
||||||
if config.duration == Some(Duration::ZERO) {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: "duration must be greater than zero; use None for continuous mode".to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if config.interval == Duration::ZERO {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: "interval must be greater than zero".to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if config.dscp > MAX_DSCP_CODEPOINT {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: format!("dscp must be <= {MAX_DSCP_CODEPOINT}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if config.length > MAX_UDP_PAYLOAD_LENGTH {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: format!("packet length must be <= {MAX_UDP_PAYLOAD_LENGTH}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(fill) = &config.server_fill {
|
|
||||||
let len = fill.len();
|
|
||||||
if len == 0 {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: "server_fill must not be empty".to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if len > MAX_SERVER_FILL_BYTES {
|
|
||||||
return Err(ClientError::InvalidConfig {
|
|
||||||
reason: format!("server_fill must be <= {MAX_SERVER_FILL_BYTES} bytes, got {len}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_duration_to_ns(field: &str, duration: Duration) -> Result<i64, ClientError> {
|
|
||||||
i64::try_from(duration.as_nanos()).map_err(|_| ClientError::InvalidConfig {
|
|
||||||
reason: format!("{field} is too large to encode as nanoseconds"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn negotiated_end_mono(start: Instant, duration_ns: i64) -> Result<Instant, ClientError> {
|
|
||||||
debug_assert!(duration_ns > 0);
|
|
||||||
let duration_ns = u64::try_from(duration_ns).expect("validated positive negotiated duration");
|
|
||||||
start
|
|
||||||
.checked_add(Duration::from_nanos(duration_ns))
|
|
||||||
.ok_or_else(|| ClientError::NegotiationRejected {
|
|
||||||
reason: "duration is too large to schedule".to_owned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -118,6 +118,30 @@ pub struct ClientConfig {
|
|||||||
pub max_pending_probes: usize,
|
pub max_pending_probes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Authentication settings used by client sessions.
|
||||||
|
///
|
||||||
|
/// `ClientConfig` keeps its existing top-level HMAC field for source
|
||||||
|
/// compatibility. Managed multi-target callers can use this smaller type for
|
||||||
|
/// per-target auth overrides without carrying a full independent
|
||||||
|
/// [`ClientConfig`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct ClientAuthConfig {
|
||||||
|
/// Optional HMAC key used to authenticate IRTT packets.
|
||||||
|
pub hmac_key: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<Vec<u8>>> for ClientAuthConfig {
|
||||||
|
fn from(hmac_key: Option<Vec<u8>>) -> Self {
|
||||||
|
Self { hmac_key }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ClientAuthConfig> for Option<Vec<u8>> {
|
||||||
|
fn from(auth: ClientAuthConfig) -> Self {
|
||||||
|
auth.hmac_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ClientConfig {
|
impl Default for ClientConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ mod managed;
|
|||||||
mod metadata;
|
mod metadata;
|
||||||
mod probe;
|
mod probe;
|
||||||
mod receive;
|
mod receive;
|
||||||
|
mod runtime;
|
||||||
mod session;
|
mod session;
|
||||||
mod socket;
|
mod socket;
|
||||||
mod socket_options;
|
mod socket_options;
|
||||||
@@ -66,8 +67,8 @@ mod timing;
|
|||||||
|
|
||||||
pub use client::Client;
|
pub use client::Client;
|
||||||
pub use config::{
|
pub use config::{
|
||||||
ClientConfig, NegotiationPolicy, RecvBudget, RunMode, SocketConfig, MAX_DSCP_CODEPOINT,
|
ClientAuthConfig, ClientConfig, NegotiationPolicy, RecvBudget, RunMode, SocketConfig,
|
||||||
MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH,
|
MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH,
|
||||||
};
|
};
|
||||||
pub use error::{ClientError, EventSubscriptionError};
|
pub use error::{ClientError, EventSubscriptionError};
|
||||||
pub use event::{
|
pub use event::{
|
||||||
@@ -75,8 +76,11 @@ pub use event::{
|
|||||||
ServerTiming, SignedDuration, WarningKind,
|
ServerTiming, SignedDuration, WarningKind,
|
||||||
};
|
};
|
||||||
pub use managed::{
|
pub use managed::{
|
||||||
CancellationToken, EventHub, EventSubscription, ManagedClient, ManagedClientSession,
|
CancellationToken, EventHub, EventSubscription, ManagedClient, ManagedClientGroup,
|
||||||
SessionEndReason, SessionOutcome, SubscriberConfig, SubscriberOverflow,
|
ManagedClientGroupConfig, ManagedClientGroupSession, ManagedClientSession,
|
||||||
|
ManagedGroupEndReason, ManagedGroupOutcome, ManagedGroupPacing, ManagedTargetConfig,
|
||||||
|
ManagedTargetEndReason, ManagedTargetOutcome, SessionEndReason, SessionOutcome,
|
||||||
|
SubscriberConfig, SubscriberOverflow, TargetEvent, TargetEventSubscription, TargetId,
|
||||||
};
|
};
|
||||||
pub use session::{NegotiatedParams, NegotiationRestriction};
|
pub use session::{NegotiatedParams, NegotiationRestriction};
|
||||||
pub use timing::ClientTimestamp;
|
pub use timing::ClientTimestamp;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -45,20 +45,20 @@ pub enum SubscriberOverflow {
|
|||||||
Disconnect,
|
Disconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish/subscribe fan-out for [`ClientEvent`] values.
|
/// Publish/subscribe fan-out for managed event values.
|
||||||
///
|
///
|
||||||
/// `EventHub` is used by managed sessions internally and is also exported for
|
/// `EventHub` is used by managed sessions internally and is also exported for
|
||||||
/// callers that want the same bounded subscription behavior around their own
|
/// callers that want the same bounded subscription behavior around their own
|
||||||
/// event producer. Publishing clones each event once per subscriber.
|
/// event producer. Publishing clones each event once per subscriber.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct EventHub {
|
pub struct EventHub<T = ClientEvent> {
|
||||||
inner: Arc<HubInner>,
|
inner: Arc<HubInner<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct HubInner {
|
struct HubInner<T> {
|
||||||
next_id: AtomicU64,
|
next_id: AtomicU64,
|
||||||
subscribers: Mutex<HashMap<u64, Arc<SubscriberInner>>>,
|
subscribers: Mutex<HashMap<u64, Arc<SubscriberInner<T>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle for receiving managed client events.
|
/// Handle for receiving managed client events.
|
||||||
@@ -68,26 +68,34 @@ struct HubInner {
|
|||||||
/// when that queue is full. Dropping the handle unregisters the subscriber.
|
/// when that queue is full. Dropping the handle unregisters the subscriber.
|
||||||
#[must_use = "dropping the subscription unregisters it"]
|
#[must_use = "dropping the subscription unregisters it"]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EventSubscription {
|
pub struct EventSubscription<T = ClientEvent> {
|
||||||
id: u64,
|
id: u64,
|
||||||
hub: Weak<HubInner>,
|
hub: Weak<HubInner<T>>,
|
||||||
inner: Arc<SubscriberInner>,
|
inner: Arc<SubscriberInner<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct SubscriberInner {
|
struct SubscriberInner<T> {
|
||||||
state: Mutex<SubscriberState>,
|
state: Mutex<SubscriberState<T>>,
|
||||||
available: Condvar,
|
available: Condvar,
|
||||||
config: SubscriberConfig,
|
config: SubscriberConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct SubscriberState {
|
struct SubscriberState<T> {
|
||||||
queue: VecDeque<ClientEvent>,
|
queue: VecDeque<T>,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHub {
|
impl<T> Clone for EventHub<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> EventHub<T> {
|
||||||
/// Create an empty event hub with no subscribers.
|
/// Create an empty event hub with no subscribers.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -104,7 +112,7 @@ impl EventHub {
|
|||||||
pub fn subscribe(
|
pub fn subscribe(
|
||||||
&self,
|
&self,
|
||||||
config: SubscriberConfig,
|
config: SubscriberConfig,
|
||||||
) -> Result<EventSubscription, crate::ClientError> {
|
) -> Result<EventSubscription<T>, crate::ClientError> {
|
||||||
if config.capacity == 0 {
|
if config.capacity == 0 {
|
||||||
return Err(crate::ClientError::InvalidConfig {
|
return Err(crate::ClientError::InvalidConfig {
|
||||||
reason: "subscriber capacity must be greater than zero".to_owned(),
|
reason: "subscriber capacity must be greater than zero".to_owned(),
|
||||||
@@ -133,13 +141,41 @@ impl EventHub {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disconnect all subscribers after leaving their already queued events
|
||||||
|
/// available to drain.
|
||||||
|
pub fn disconnect_all(&self) {
|
||||||
|
let subscribers: Vec<Arc<SubscriberInner<T>>> = self
|
||||||
|
.inner
|
||||||
|
.subscribers
|
||||||
|
.lock()
|
||||||
|
.expect("event hub mutex poisoned")
|
||||||
|
.drain()
|
||||||
|
.map(|(_, subscriber)| subscriber)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for subscriber in subscribers {
|
||||||
|
subscriber.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn subscriber_count(&self) -> usize {
|
||||||
|
self.inner
|
||||||
|
.subscribers
|
||||||
|
.lock()
|
||||||
|
.expect("event hub mutex poisoned")
|
||||||
|
.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> EventHub<T> {
|
||||||
/// Publish an event to all currently connected subscribers.
|
/// Publish an event to all currently connected subscribers.
|
||||||
///
|
///
|
||||||
/// Slow subscribers are handled according to their
|
/// Slow subscribers are handled according to their
|
||||||
/// [`SubscriberOverflow`] policy. Publishing does not block waiting for a
|
/// [`SubscriberOverflow`] policy. Publishing does not block waiting for a
|
||||||
/// subscriber to consume queued events.
|
/// subscriber to consume queued events.
|
||||||
pub fn publish(&self, event: ClientEvent) {
|
pub fn publish(&self, event: T) {
|
||||||
let subscribers: Vec<(u64, Arc<SubscriberInner>)> = self
|
let subscribers: Vec<(u64, Arc<SubscriberInner<T>>)> = self
|
||||||
.inner
|
.inner
|
||||||
.subscribers
|
.subscribers
|
||||||
.lock()
|
.lock()
|
||||||
@@ -170,47 +206,21 @@ impl EventHub {
|
|||||||
subscribers.remove(&id);
|
subscribers.remove(&id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disconnect all subscribers after leaving their already queued events
|
|
||||||
/// available to drain.
|
|
||||||
pub fn disconnect_all(&self) {
|
|
||||||
let subscribers: Vec<Arc<SubscriberInner>> = self
|
|
||||||
.inner
|
|
||||||
.subscribers
|
|
||||||
.lock()
|
|
||||||
.expect("event hub mutex poisoned")
|
|
||||||
.drain()
|
|
||||||
.map(|(_, subscriber)| subscriber)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for subscriber in subscribers {
|
|
||||||
subscriber.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn subscriber_count(&self) -> usize {
|
|
||||||
self.inner
|
|
||||||
.subscribers
|
|
||||||
.lock()
|
|
||||||
.expect("event hub mutex poisoned")
|
|
||||||
.len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EventHub {
|
impl<T> Default for EventHub<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventSubscription {
|
impl<T> EventSubscription<T> {
|
||||||
/// Block until the next queued event is available.
|
/// Block until the next queued event is available.
|
||||||
///
|
///
|
||||||
/// If the subscription is disconnected, this returns
|
/// If the subscription is disconnected, this returns
|
||||||
/// [`EventSubscriptionError::Disconnected`] after any already queued events
|
/// [`EventSubscriptionError::Disconnected`] after any already queued events
|
||||||
/// have been drained.
|
/// have been drained.
|
||||||
pub fn recv(&self) -> Result<ClientEvent, EventSubscriptionError> {
|
pub fn recv(&self) -> Result<T, EventSubscriptionError> {
|
||||||
let mut state = self.inner.state.lock().expect("subscriber mutex poisoned");
|
let mut state = self.inner.state.lock().expect("subscriber mutex poisoned");
|
||||||
loop {
|
loop {
|
||||||
if let Some(event) = state.queue.pop_front() {
|
if let Some(event) = state.queue.pop_front() {
|
||||||
@@ -233,7 +243,7 @@ impl EventSubscription {
|
|||||||
/// is queued. If the subscription is disconnected, this returns
|
/// is queued. If the subscription is disconnected, this returns
|
||||||
/// [`EventSubscriptionError::Disconnected`] after any already queued events
|
/// [`EventSubscriptionError::Disconnected`] after any already queued events
|
||||||
/// have been drained.
|
/// have been drained.
|
||||||
pub fn try_recv(&self) -> Result<Option<ClientEvent>, EventSubscriptionError> {
|
pub fn try_recv(&self) -> Result<Option<T>, EventSubscriptionError> {
|
||||||
let mut state = self.inner.state.lock().expect("subscriber mutex poisoned");
|
let mut state = self.inner.state.lock().expect("subscriber mutex poisoned");
|
||||||
if let Some(event) = state.queue.pop_front() {
|
if let Some(event) = state.queue.pop_front() {
|
||||||
return Ok(Some(event));
|
return Ok(Some(event));
|
||||||
@@ -245,7 +255,7 @@ impl EventSubscription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for EventSubscription {
|
impl<T> Drop for EventSubscription<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.inner.disconnect();
|
self.inner.disconnect();
|
||||||
if let Some(hub) = self.hub.upgrade() {
|
if let Some(hub) = self.hub.upgrade() {
|
||||||
@@ -257,8 +267,8 @@ impl Drop for EventSubscription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubscriberInner {
|
impl<T> SubscriberInner<T> {
|
||||||
fn publish(&self, event: ClientEvent) -> bool {
|
fn publish(&self, event: T) -> bool {
|
||||||
let mut state = self.state.lock().expect("subscriber mutex poisoned");
|
let mut state = self.state.lock().expect("subscriber mutex poisoned");
|
||||||
if !state.connected {
|
if !state.connected {
|
||||||
return false;
|
return false;
|
||||||
@@ -411,7 +421,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dropping_subscription_unregisters_from_hub() {
|
fn dropping_subscription_unregisters_from_hub() {
|
||||||
let hub = EventHub::new();
|
let hub: EventHub = EventHub::new();
|
||||||
let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
|
let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
|
||||||
assert_eq!(hub.subscriber_count(), 1);
|
assert_eq!(hub.subscriber_count(), 1);
|
||||||
|
|
||||||
@@ -422,7 +432,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn disconnect_all_wakes_blocking_receivers() {
|
fn disconnect_all_wakes_blocking_receivers() {
|
||||||
let hub = EventHub::new();
|
let hub: EventHub = EventHub::new();
|
||||||
let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
|
let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
|
||||||
hub.disconnect_all();
|
hub.disconnect_all();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
mod cancellation;
|
mod cancellation;
|
||||||
|
mod group;
|
||||||
mod hub;
|
mod hub;
|
||||||
mod runner;
|
mod runner;
|
||||||
|
|
||||||
pub use cancellation::CancellationToken;
|
pub use cancellation::CancellationToken;
|
||||||
|
pub use group::{
|
||||||
|
ManagedClientGroup, ManagedClientGroupConfig, ManagedClientGroupSession, ManagedGroupEndReason,
|
||||||
|
ManagedGroupOutcome, ManagedGroupPacing, ManagedTargetConfig, ManagedTargetEndReason,
|
||||||
|
ManagedTargetOutcome, TargetEvent, TargetEventSubscription, TargetId,
|
||||||
|
};
|
||||||
pub use hub::{EventHub, EventSubscription, SubscriberConfig, SubscriberOverflow};
|
pub use hub::{EventHub, EventSubscription, SubscriberConfig, SubscriberOverflow};
|
||||||
pub use runner::{ManagedClient, ManagedClientSession, SessionEndReason, SessionOutcome};
|
pub use runner::{ManagedClient, ManagedClientSession, SessionEndReason, SessionOutcome};
|
||||||
@@ -15,6 +15,14 @@ pub(crate) struct ReceivedDatagram {
|
|||||||
pub(crate) meta: ReceiveMeta,
|
pub(crate) meta: ReceiveMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct ReceivedDatagramFrom {
|
||||||
|
pub(crate) len: usize,
|
||||||
|
pub(crate) source: SocketAddr,
|
||||||
|
pub(crate) received_at: ClientTimestamp,
|
||||||
|
pub(crate) meta: ReceiveMeta,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(all(target_os = "linux", feature = "ancillary")))]
|
#[cfg(not(all(target_os = "linux", feature = "ancillary")))]
|
||||||
pub(crate) fn recv_datagram(
|
pub(crate) fn recv_datagram(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
@@ -30,6 +38,22 @@ pub(crate) fn recv_datagram(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(all(target_os = "linux", feature = "ancillary")))]
|
||||||
|
pub(crate) fn recv_datagram_from(
|
||||||
|
socket: &UdpSocket,
|
||||||
|
buf: &mut [u8],
|
||||||
|
) -> Result<ReceivedDatagramFrom, io::Error> {
|
||||||
|
let (len, source) = socket.recv_from(buf)?;
|
||||||
|
let received_at = ClientTimestamp::now();
|
||||||
|
|
||||||
|
Ok(ReceivedDatagramFrom {
|
||||||
|
len,
|
||||||
|
source,
|
||||||
|
received_at,
|
||||||
|
meta: ReceiveMeta::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(target_os = "linux", feature = "ancillary"))]
|
#[cfg(all(target_os = "linux", feature = "ancillary"))]
|
||||||
pub(crate) fn recv_datagram(
|
pub(crate) fn recv_datagram(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
@@ -38,6 +62,14 @@ pub(crate) fn recv_datagram(
|
|||||||
linux::recv_datagram(socket, buf)
|
linux::recv_datagram(socket, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", feature = "ancillary"))]
|
||||||
|
pub(crate) fn recv_datagram_from(
|
||||||
|
socket: &UdpSocket,
|
||||||
|
buf: &mut [u8],
|
||||||
|
) -> Result<ReceivedDatagramFrom, io::Error> {
|
||||||
|
linux::recv_datagram_from(socket, buf)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(all(target_os = "linux", feature = "ancillary")))]
|
#[cfg(not(all(target_os = "linux", feature = "ancillary")))]
|
||||||
pub(crate) fn configure_receive_metadata(
|
pub(crate) fn configure_receive_metadata(
|
||||||
_socket: &UdpSocket,
|
_socket: &UdpSocket,
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
#![allow(unsafe_code)]
|
#![allow(unsafe_code)]
|
||||||
use std::{
|
use std::{
|
||||||
io, mem,
|
io, mem,
|
||||||
net::{SocketAddr, UdpSocket},
|
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket},
|
||||||
os::fd::AsRawFd,
|
os::fd::AsRawFd,
|
||||||
ptr,
|
ptr,
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{metadata::ReceiveMeta, receive::ReceivedDatagram, timing::ClientTimestamp};
|
use crate::{
|
||||||
|
metadata::ReceiveMeta,
|
||||||
|
receive::{ReceivedDatagram, ReceivedDatagramFrom},
|
||||||
|
timing::ClientTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
const CONTROL_LEN: usize = 128;
|
const CONTROL_LEN: usize = 128;
|
||||||
|
|
||||||
@@ -87,6 +91,61 @@ pub(crate) fn recv_datagram(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn recv_datagram_from(
|
||||||
|
socket: &UdpSocket,
|
||||||
|
buf: &mut [u8],
|
||||||
|
) -> Result<ReceivedDatagramFrom, io::Error> {
|
||||||
|
let mut control = ControlBuffer::new();
|
||||||
|
let mut name: libc::sockaddr_storage = unsafe { mem::zeroed() };
|
||||||
|
let mut iov = libc::iovec {
|
||||||
|
iov_base: buf.as_mut_ptr().cast(),
|
||||||
|
iov_len: buf.len(),
|
||||||
|
};
|
||||||
|
let mut msg: libc::msghdr = unsafe { mem::zeroed() };
|
||||||
|
msg.msg_name = (&mut name as *mut libc::sockaddr_storage).cast();
|
||||||
|
msg.msg_namelen = mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||||
|
msg.msg_iov = &mut iov;
|
||||||
|
msg.msg_iovlen = 1;
|
||||||
|
msg.msg_control = control.as_mut_ptr().cast();
|
||||||
|
msg.msg_controllen = control.len();
|
||||||
|
|
||||||
|
let len = unsafe {
|
||||||
|
// SAFETY: `msg` points to one writable iovec backed by `buf`, a
|
||||||
|
// writable source-address buffer, and a writable control buffer. All
|
||||||
|
// buffers live until `recvmsg` returns. The socket file descriptor is
|
||||||
|
// borrowed from a valid `UdpSocket`.
|
||||||
|
libc::recvmsg(socket.as_raw_fd(), &mut msg, 0)
|
||||||
|
};
|
||||||
|
if len < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
let len = usize::try_from(len).map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"recvmsg returned an unrepresentable datagram length",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let received_at = ClientTimestamp::now();
|
||||||
|
let source = unsafe {
|
||||||
|
// SAFETY: `name` and `msg_namelen` were initialized by a successful
|
||||||
|
// `recvmsg` call above.
|
||||||
|
socket_addr_from_storage(&name, msg.msg_namelen)?
|
||||||
|
};
|
||||||
|
let meta = unsafe {
|
||||||
|
// SAFETY: `msg` was initialized by a successful `recvmsg` call. The
|
||||||
|
// parser only reads cmsghdr entries within `msg_controllen` and ignores
|
||||||
|
// short or unrelated control messages.
|
||||||
|
parse_receive_meta(&msg)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ReceivedDatagramFrom {
|
||||||
|
len,
|
||||||
|
source,
|
||||||
|
received_at,
|
||||||
|
meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
unsafe fn parse_receive_meta(msg: &libc::msghdr) -> ReceiveMeta {
|
unsafe fn parse_receive_meta(msg: &libc::msghdr) -> ReceiveMeta {
|
||||||
let mut meta = ReceiveMeta::default();
|
let mut meta = ReceiveMeta::default();
|
||||||
let mut cmsg = libc::CMSG_FIRSTHDR(msg);
|
let mut cmsg = libc::CMSG_FIRSTHDR(msg);
|
||||||
@@ -108,6 +167,52 @@ unsafe fn parse_receive_meta(msg: &libc::msghdr) -> ReceiveMeta {
|
|||||||
meta
|
meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe fn socket_addr_from_storage(
|
||||||
|
storage: &libc::sockaddr_storage,
|
||||||
|
len: libc::socklen_t,
|
||||||
|
) -> io::Result<SocketAddr> {
|
||||||
|
match i32::from(storage.ss_family) {
|
||||||
|
libc::AF_INET => {
|
||||||
|
if usize::try_from(len).unwrap_or(0) < mem::size_of::<libc::sockaddr_in>() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"short IPv4 source address from recvmsg",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let addr = ptr::read_unaligned(
|
||||||
|
(storage as *const libc::sockaddr_storage).cast::<libc::sockaddr_in>(),
|
||||||
|
);
|
||||||
|
let ip = Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr));
|
||||||
|
let port = u16::from_be(addr.sin_port);
|
||||||
|
Ok(SocketAddr::V4(SocketAddrV4::new(ip, port)))
|
||||||
|
}
|
||||||
|
libc::AF_INET6 => {
|
||||||
|
if usize::try_from(len).unwrap_or(0) < mem::size_of::<libc::sockaddr_in6>() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"short IPv6 source address from recvmsg",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let addr = ptr::read_unaligned(
|
||||||
|
(storage as *const libc::sockaddr_storage).cast::<libc::sockaddr_in6>(),
|
||||||
|
);
|
||||||
|
let ip = Ipv6Addr::from(addr.sin6_addr.s6_addr);
|
||||||
|
let port = u16::from_be(addr.sin6_port);
|
||||||
|
let flowinfo = u32::from_be(addr.sin6_flowinfo);
|
||||||
|
Ok(SocketAddr::V6(SocketAddrV6::new(
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
flowinfo,
|
||||||
|
addr.sin6_scope_id,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
family => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("unsupported source address family from recvmsg: {family}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_traffic_class_cmsg(cmsg: &libc::cmsghdr) -> bool {
|
fn is_traffic_class_cmsg(cmsg: &libc::cmsghdr) -> bool {
|
||||||
(cmsg.cmsg_level == libc::IPPROTO_IP && cmsg.cmsg_type == libc::IP_TOS)
|
(cmsg.cmsg_level == libc::IPPROTO_IP && cmsg.cmsg_type == libc::IP_TOS)
|
||||||
|| (cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == libc::IPV6_TCLASS)
|
|| (cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == libc::IPV6_TCLASS)
|
||||||
|
|||||||
@@ -0,0 +1,864 @@
|
|||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use irtt_proto::{
|
||||||
|
close::CloseRequest, decode_echo_reply, echo_packet_len, encode_close_request,
|
||||||
|
encode_echo_request, encode_open_request, flags, EchoReply, EchoRequest, OpenReply,
|
||||||
|
OpenRequest, Params, ServerFill, TimestampFields, PROTOCOL_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{
|
||||||
|
ClientConfig, RunMode, MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_UDP_PAYLOAD_LENGTH,
|
||||||
|
},
|
||||||
|
error::ClientError,
|
||||||
|
event::{
|
||||||
|
ClientEvent, OneWayDelaySample, OpenOutcome, ReceivedStatsSample, RttSample, ServerTiming,
|
||||||
|
SignedDuration, WarningKind,
|
||||||
|
},
|
||||||
|
metadata::ReceiveMeta,
|
||||||
|
probe::{CompletedSet, PendingMap, PendingProbe, TimedOutMap},
|
||||||
|
session::{negotiate_params, ActiveSession, ClientPhase, CloseSource, NegotiatedParams},
|
||||||
|
timing::ClientTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) const MAX_OPEN_PACKET_SIZE: usize = 512;
|
||||||
|
const MIN_RECV_BUFFER_SIZE: usize = 2048;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct SessionRuntime {
|
||||||
|
config: ClientConfig,
|
||||||
|
remote: std::net::SocketAddr,
|
||||||
|
requested: Params,
|
||||||
|
negotiated: Option<NegotiatedParams>,
|
||||||
|
phase: ClientPhase,
|
||||||
|
session: Option<ActiveSession>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct SendProbeResult {
|
||||||
|
pub(crate) sent_at: ClientTimestamp,
|
||||||
|
pub(crate) bytes: usize,
|
||||||
|
pub(crate) send_call: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionRuntime {
|
||||||
|
pub(crate) fn new(
|
||||||
|
config: ClientConfig,
|
||||||
|
remote: std::net::SocketAddr,
|
||||||
|
) -> Result<Self, ClientError> {
|
||||||
|
if config.max_pending_probes == 0 {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: "max_pending_probes must be greater than zero".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if config.probe_timeout == Duration::ZERO {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: "probe_timeout must be greater than zero".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let requested = params_from_config(&config)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
remote,
|
||||||
|
requested,
|
||||||
|
negotiated: None,
|
||||||
|
phase: ClientPhase::Connected,
|
||||||
|
session: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn config(&self) -> &ClientConfig {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_hmac(&self) -> bool {
|
||||||
|
self.config.hmac_key.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn open_packet(&self) -> Result<Vec<u8>, ClientError> {
|
||||||
|
match self.phase {
|
||||||
|
ClientPhase::Connected => {}
|
||||||
|
ClientPhase::Open { .. } => return Err(ClientError::AlreadyOpen),
|
||||||
|
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
||||||
|
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = OpenRequest {
|
||||||
|
params: self.requested.clone(),
|
||||||
|
close: self.config.run_mode == RunMode::NoTest,
|
||||||
|
};
|
||||||
|
Ok(encode_open_request(
|
||||||
|
&request,
|
||||||
|
self.config.hmac_key.as_deref(),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decode_open_reply(&self, packet: &[u8]) -> Result<OpenReply, ClientError> {
|
||||||
|
Ok(irtt_proto::decode_open_reply(
|
||||||
|
packet,
|
||||||
|
self.config.hmac_key.as_deref(),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn accept_open_reply<F>(
|
||||||
|
&mut self,
|
||||||
|
reply: OpenReply,
|
||||||
|
now: ClientTimestamp,
|
||||||
|
before_normal_open: F,
|
||||||
|
) -> Result<OpenOutcome, ClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&NegotiatedParams) -> Result<(), ClientError>,
|
||||||
|
{
|
||||||
|
if reply.params.protocol_version != PROTOCOL_VERSION {
|
||||||
|
return Err(ClientError::ProtocolVersionMismatch {
|
||||||
|
requested: PROTOCOL_VERSION,
|
||||||
|
received: reply.params.protocol_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let reply_is_close = flags::has(reply.flags, flags::FLAG_CLOSE);
|
||||||
|
match self.config.run_mode {
|
||||||
|
RunMode::Normal if reply_is_close => Err(ClientError::ServerRejected),
|
||||||
|
RunMode::Normal if reply.token == 0 => Err(ClientError::ZeroToken),
|
||||||
|
RunMode::Normal => self.accept_normal_open(reply, now, before_normal_open),
|
||||||
|
RunMode::NoTest if !reply_is_close => Err(ClientError::UnexpectedNoTestReply),
|
||||||
|
RunMode::NoTest if reply.token != 0 => {
|
||||||
|
Err(ClientError::NonZeroNoTestToken { token: reply.token })
|
||||||
|
}
|
||||||
|
RunMode::NoTest => self.accept_no_test_open(reply, now),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_send_deadline(&self) -> Option<Instant> {
|
||||||
|
let session = self.session.as_ref()?;
|
||||||
|
if session.sending_done {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(session.next_send_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn probe_timeout(&self) -> Duration {
|
||||||
|
self.config.probe_timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send_probe_with<F>(
|
||||||
|
&mut self,
|
||||||
|
override_ts: Option<ClientTimestamp>,
|
||||||
|
send: F,
|
||||||
|
) -> Result<Vec<ClientEvent>, ClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&[u8]) -> Result<SendProbeResult, ClientError>,
|
||||||
|
{
|
||||||
|
self.send_probe_inner(override_ts, None, send)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send_probe_for_deadline<F>(
|
||||||
|
&mut self,
|
||||||
|
scheduled_at: Instant,
|
||||||
|
send: F,
|
||||||
|
) -> Result<Vec<ClientEvent>, ClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&[u8]) -> Result<SendProbeResult, ClientError>,
|
||||||
|
{
|
||||||
|
self.send_probe_inner(None, Some(scheduled_at), send)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_probe_inner<F>(
|
||||||
|
&mut self,
|
||||||
|
override_ts: Option<ClientTimestamp>,
|
||||||
|
scheduled_at_override: Option<Instant>,
|
||||||
|
send: F,
|
||||||
|
) -> Result<Vec<ClientEvent>, ClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&[u8]) -> Result<SendProbeResult, ClientError>,
|
||||||
|
{
|
||||||
|
let token = match self.phase {
|
||||||
|
ClientPhase::Open { token } => token,
|
||||||
|
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
||||||
|
ClientPhase::Connected => return Err(ClientError::NotOpen),
|
||||||
|
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
||||||
|
};
|
||||||
|
|
||||||
|
let session = self
|
||||||
|
.session
|
||||||
|
.as_mut()
|
||||||
|
.expect("session must exist when phase is Open");
|
||||||
|
|
||||||
|
if session.sending_done {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = override_ts.unwrap_or_else(ClientTimestamp::now);
|
||||||
|
|
||||||
|
if let Some(end) = session.end_mono {
|
||||||
|
if now.mono >= end {
|
||||||
|
session.sending_done = true;
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.pending.check_capacity()?;
|
||||||
|
|
||||||
|
let negotiated = self
|
||||||
|
.negotiated
|
||||||
|
.as_ref()
|
||||||
|
.expect("negotiated must exist when Open");
|
||||||
|
|
||||||
|
let wire_seq = session.next_wire_seq;
|
||||||
|
let scheduled_at = scheduled_at_override.unwrap_or(session.next_send_at);
|
||||||
|
|
||||||
|
let request = EchoRequest {
|
||||||
|
token,
|
||||||
|
sequence: wire_seq,
|
||||||
|
params: negotiated.params.clone(),
|
||||||
|
payload: vec![],
|
||||||
|
};
|
||||||
|
let packet = encode_echo_request(&request, self.config.hmac_key.as_deref())?;
|
||||||
|
let send_result = send(&packet)?;
|
||||||
|
let timer_error = instant_abs_diff(send_result.sent_at.mono, scheduled_at);
|
||||||
|
|
||||||
|
let session = self
|
||||||
|
.session
|
||||||
|
.as_mut()
|
||||||
|
.expect("session must exist when phase is Open");
|
||||||
|
|
||||||
|
let pending = PendingProbe {
|
||||||
|
wire_seq,
|
||||||
|
sent_at: send_result.sent_at,
|
||||||
|
timeout_at: send_result
|
||||||
|
.sent_at
|
||||||
|
.mono
|
||||||
|
.checked_add(self.config.probe_timeout)
|
||||||
|
.ok_or(ClientError::DurationOverflow)?,
|
||||||
|
};
|
||||||
|
session.pending.insert(pending)?;
|
||||||
|
|
||||||
|
session.next_wire_seq = session.next_wire_seq.wrapping_add(1);
|
||||||
|
session.packets_sent =
|
||||||
|
session
|
||||||
|
.packets_sent
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or(ClientError::CounterOverflow {
|
||||||
|
counter: "packets_sent",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let negotiated = self
|
||||||
|
.negotiated
|
||||||
|
.as_ref()
|
||||||
|
.expect("negotiated must exist when Open");
|
||||||
|
let interval_ns = u64::try_from(negotiated.params.interval_ns)
|
||||||
|
.expect("validated positive negotiated interval");
|
||||||
|
let session = self
|
||||||
|
.session
|
||||||
|
.as_mut()
|
||||||
|
.expect("session must exist when phase is Open");
|
||||||
|
session.next_send_at = if let Some(scheduled_at) = scheduled_at_override {
|
||||||
|
scheduled_at
|
||||||
|
.checked_add(Duration::from_nanos(interval_ns))
|
||||||
|
.ok_or(ClientError::DurationOverflow)?
|
||||||
|
} else {
|
||||||
|
next_probe_deadline(session.start_mono, interval_ns, session.packets_sent)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(end) = session.end_mono {
|
||||||
|
if session.next_send_at >= end {
|
||||||
|
session.sending_done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vec![ClientEvent::EchoSent {
|
||||||
|
seq: wire_seq,
|
||||||
|
remote: self.remote,
|
||||||
|
scheduled_at,
|
||||||
|
sent_at: send_result.sent_at,
|
||||||
|
bytes: send_result.bytes,
|
||||||
|
send_call: send_result.send_call,
|
||||||
|
timer_error,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process_received_echo_packet(
|
||||||
|
&mut self,
|
||||||
|
packet: &[u8],
|
||||||
|
now: ClientTimestamp,
|
||||||
|
meta: ReceiveMeta,
|
||||||
|
) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
|
match self.phase {
|
||||||
|
ClientPhase::Open { .. } => {}
|
||||||
|
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
||||||
|
ClientPhase::Connected => return Err(ClientError::NotOpen),
|
||||||
|
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(reply) = self.decode_received_packet(packet) else {
|
||||||
|
return Ok(vec![ClientEvent::Warning {
|
||||||
|
kind: WarningKind::MalformedOrUnrelatedPacket,
|
||||||
|
message: "dropped malformed or unrelated packet".to_owned(),
|
||||||
|
at: now,
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
self.process_echo_reply(reply, packet.len(), now, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn poll_timeouts_at(
|
||||||
|
&mut self,
|
||||||
|
now: Instant,
|
||||||
|
) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
|
match self.phase {
|
||||||
|
ClientPhase::Open { .. } => {}
|
||||||
|
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
||||||
|
ClientPhase::Connected => return Err(ClientError::NotOpen),
|
||||||
|
ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = self
|
||||||
|
.session
|
||||||
|
.as_mut()
|
||||||
|
.expect("session must exist when phase is Open");
|
||||||
|
|
||||||
|
let expired = session.pending.drain_expired(now);
|
||||||
|
let mut events = Vec::with_capacity(expired.len());
|
||||||
|
for probe in expired {
|
||||||
|
events.push(ClientEvent::EchoLoss {
|
||||||
|
seq: probe.wire_seq,
|
||||||
|
sent_at: probe.sent_at,
|
||||||
|
timeout_at: probe.timeout_at,
|
||||||
|
});
|
||||||
|
session.timed_out.insert(probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn close_with<F>(&mut self, send: F) -> Result<Vec<ClientEvent>, ClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&[u8]) -> Result<(), ClientError>,
|
||||||
|
{
|
||||||
|
let token = match self.phase {
|
||||||
|
ClientPhase::Open { token } => token,
|
||||||
|
ClientPhase::Closed { .. } => return Err(ClientError::AlreadyClosed),
|
||||||
|
ClientPhase::Connected | ClientPhase::NoTestCompleted => {
|
||||||
|
return Err(ClientError::NotOpen)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let packet =
|
||||||
|
encode_close_request(&CloseRequest { token }, self.config.hmac_key.as_deref())?;
|
||||||
|
send(&packet)?;
|
||||||
|
self.phase = ClientPhase::Closed {
|
||||||
|
source: CloseSource::Local,
|
||||||
|
};
|
||||||
|
if let Some(session) = self.session.as_mut() {
|
||||||
|
session.timed_out.clear();
|
||||||
|
}
|
||||||
|
self.session = None;
|
||||||
|
|
||||||
|
Ok(vec![ClientEvent::SessionClosed {
|
||||||
|
remote: self.remote,
|
||||||
|
token,
|
||||||
|
at: ClientTimestamp::now(),
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_run_complete(&self) -> bool {
|
||||||
|
let Some(session) = self.session.as_ref() else {
|
||||||
|
return matches!(
|
||||||
|
self.phase,
|
||||||
|
ClientPhase::Closed { .. } | ClientPhase::NoTestCompleted
|
||||||
|
);
|
||||||
|
};
|
||||||
|
session.sending_done && session.pending.len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_peer_closed(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.phase,
|
||||||
|
ClientPhase::Closed {
|
||||||
|
source: CloseSource::Peer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_timed_out_metadata(&self) -> bool {
|
||||||
|
self.session
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|session| session.timed_out.len() > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn packets_sent(&self) -> u64 {
|
||||||
|
self.session
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |session| session.packets_sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_open(&self) -> bool {
|
||||||
|
matches!(self.phase, ClientPhase::Open { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_normal_open<F>(
|
||||||
|
&mut self,
|
||||||
|
reply: OpenReply,
|
||||||
|
now: ClientTimestamp,
|
||||||
|
before_normal_open: F,
|
||||||
|
) -> Result<OpenOutcome, ClientError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&NegotiatedParams) -> Result<(), ClientError>,
|
||||||
|
{
|
||||||
|
let token = reply.token;
|
||||||
|
let negotiated = negotiate_params(
|
||||||
|
&self.requested,
|
||||||
|
reply.params,
|
||||||
|
self.config.negotiation_policy,
|
||||||
|
)?;
|
||||||
|
before_normal_open(&negotiated)?;
|
||||||
|
self.negotiated = Some(negotiated.clone());
|
||||||
|
self.phase = ClientPhase::Open { token };
|
||||||
|
|
||||||
|
let end_mono = if negotiated.params.duration_ns > 0 {
|
||||||
|
Some(negotiated_end_mono(
|
||||||
|
now.mono,
|
||||||
|
negotiated.params.duration_ns,
|
||||||
|
)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
self.session = Some(ActiveSession {
|
||||||
|
next_wire_seq: 0,
|
||||||
|
highest_received_seq: None,
|
||||||
|
packets_sent: 0,
|
||||||
|
start_mono: now.mono,
|
||||||
|
end_mono,
|
||||||
|
next_send_at: now.mono,
|
||||||
|
pending: PendingMap::new(self.config.max_pending_probes),
|
||||||
|
timed_out: TimedOutMap::new(self.config.max_pending_probes),
|
||||||
|
completed: CompletedSet::new(self.config.max_pending_probes),
|
||||||
|
sending_done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = ClientEvent::SessionStarted {
|
||||||
|
remote: self.remote,
|
||||||
|
token,
|
||||||
|
negotiated: negotiated.clone(),
|
||||||
|
at: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(OpenOutcome::Started {
|
||||||
|
remote: self.remote,
|
||||||
|
token,
|
||||||
|
negotiated,
|
||||||
|
event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_no_test_open(
|
||||||
|
&mut self,
|
||||||
|
reply: OpenReply,
|
||||||
|
now: ClientTimestamp,
|
||||||
|
) -> Result<OpenOutcome, ClientError> {
|
||||||
|
let negotiated = negotiate_params(
|
||||||
|
&self.requested,
|
||||||
|
reply.params,
|
||||||
|
self.config.negotiation_policy,
|
||||||
|
)?;
|
||||||
|
self.negotiated = Some(negotiated.clone());
|
||||||
|
self.phase = ClientPhase::NoTestCompleted;
|
||||||
|
let event = ClientEvent::NoTestCompleted {
|
||||||
|
remote: self.remote,
|
||||||
|
negotiated: negotiated.clone(),
|
||||||
|
at: now,
|
||||||
|
};
|
||||||
|
Ok(OpenOutcome::NoTestCompleted {
|
||||||
|
remote: self.remote,
|
||||||
|
negotiated,
|
||||||
|
event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_received_packet(&self, packet: &[u8]) -> Option<EchoReply> {
|
||||||
|
let negotiated = self
|
||||||
|
.negotiated
|
||||||
|
.as_ref()
|
||||||
|
.expect("negotiated must exist when Open");
|
||||||
|
|
||||||
|
decode_echo_reply(packet, &negotiated.params, self.config.hmac_key.as_deref()).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_echo_reply(
|
||||||
|
&mut self,
|
||||||
|
reply: EchoReply,
|
||||||
|
packet_len: usize,
|
||||||
|
now: ClientTimestamp,
|
||||||
|
meta: ReceiveMeta,
|
||||||
|
) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
|
let token = match self.phase {
|
||||||
|
ClientPhase::Open { token } => token,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
if reply.token != token {
|
||||||
|
return Ok(vec![ClientEvent::Warning {
|
||||||
|
kind: WarningKind::WrongToken,
|
||||||
|
message: format!(
|
||||||
|
"dropped reply with wrong token: expected {token:#x}, got {:#x}",
|
||||||
|
reply.token
|
||||||
|
),
|
||||||
|
at: now,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = self.session.as_mut().expect("session must exist when Open");
|
||||||
|
|
||||||
|
let wire_seq = reply.sequence;
|
||||||
|
|
||||||
|
if let Some(pending) = session.pending.remove(wire_seq) {
|
||||||
|
let rtt = compute_rtt(&pending.sent_at, &now, &reply.timestamps);
|
||||||
|
let server_timing = build_server_timing(&reply.timestamps);
|
||||||
|
let one_way = compute_one_way(&pending.sent_at, &now, &reply.timestamps);
|
||||||
|
let received_stats = build_received_stats(&reply);
|
||||||
|
let is_late = session
|
||||||
|
.highest_received_seq
|
||||||
|
.is_some_and(|h| sequence_is_before(wire_seq, h));
|
||||||
|
let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
|
||||||
|
|
||||||
|
update_highest_received(&mut session.highest_received_seq, wire_seq);
|
||||||
|
session.completed.insert(wire_seq);
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
if is_late {
|
||||||
|
events.push(ClientEvent::LateReply {
|
||||||
|
seq: wire_seq,
|
||||||
|
highest_seen,
|
||||||
|
remote: self.remote,
|
||||||
|
sent_at: Some(pending.sent_at),
|
||||||
|
received_at: now,
|
||||||
|
rtt: Some(rtt),
|
||||||
|
server_timing,
|
||||||
|
one_way,
|
||||||
|
received_stats,
|
||||||
|
bytes: packet_len,
|
||||||
|
packet_meta: meta.into(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
events.push(ClientEvent::EchoReply {
|
||||||
|
seq: wire_seq,
|
||||||
|
remote: self.remote,
|
||||||
|
sent_at: pending.sent_at,
|
||||||
|
received_at: now,
|
||||||
|
rtt,
|
||||||
|
server_timing,
|
||||||
|
one_way,
|
||||||
|
received_stats,
|
||||||
|
bytes: packet_len,
|
||||||
|
packet_meta: meta.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if flags::has(reply.flags, flags::FLAG_CLOSE) {
|
||||||
|
self.close_from_peer(token, now, &mut events);
|
||||||
|
}
|
||||||
|
Ok(events)
|
||||||
|
} else if session.completed.contains(wire_seq) {
|
||||||
|
update_highest_received(&mut session.highest_received_seq, wire_seq);
|
||||||
|
Ok(vec![ClientEvent::DuplicateReply {
|
||||||
|
seq: wire_seq,
|
||||||
|
remote: self.remote,
|
||||||
|
received_at: now,
|
||||||
|
bytes: packet_len,
|
||||||
|
}])
|
||||||
|
} else if let Some(timed_out) = session.timed_out.remove(wire_seq) {
|
||||||
|
let rtt = compute_rtt(&timed_out.sent_at, &now, &reply.timestamps);
|
||||||
|
let server_timing = build_server_timing(&reply.timestamps);
|
||||||
|
let one_way = compute_one_way(&timed_out.sent_at, &now, &reply.timestamps);
|
||||||
|
let received_stats = build_received_stats(&reply);
|
||||||
|
let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
|
||||||
|
update_highest_received(&mut session.highest_received_seq, wire_seq);
|
||||||
|
session.completed.insert(wire_seq);
|
||||||
|
|
||||||
|
let mut events = vec![ClientEvent::LateReply {
|
||||||
|
seq: wire_seq,
|
||||||
|
highest_seen,
|
||||||
|
remote: self.remote,
|
||||||
|
sent_at: Some(timed_out.sent_at),
|
||||||
|
received_at: now,
|
||||||
|
rtt: Some(rtt),
|
||||||
|
server_timing,
|
||||||
|
one_way,
|
||||||
|
received_stats,
|
||||||
|
bytes: packet_len,
|
||||||
|
packet_meta: meta.into(),
|
||||||
|
}];
|
||||||
|
if flags::has(reply.flags, flags::FLAG_CLOSE) {
|
||||||
|
self.close_from_peer(token, now, &mut events);
|
||||||
|
}
|
||||||
|
Ok(events)
|
||||||
|
} else if session
|
||||||
|
.highest_received_seq
|
||||||
|
.is_some_and(|h| sequence_is_before(wire_seq, h))
|
||||||
|
{
|
||||||
|
Ok(vec![ClientEvent::LateReply {
|
||||||
|
seq: wire_seq,
|
||||||
|
highest_seen: session.highest_received_seq.unwrap(),
|
||||||
|
remote: self.remote,
|
||||||
|
sent_at: None,
|
||||||
|
received_at: now,
|
||||||
|
rtt: None,
|
||||||
|
server_timing: build_server_timing(&reply.timestamps),
|
||||||
|
one_way: None,
|
||||||
|
received_stats: build_received_stats(&reply),
|
||||||
|
bytes: packet_len,
|
||||||
|
packet_meta: meta.into(),
|
||||||
|
}])
|
||||||
|
} else {
|
||||||
|
Ok(vec![ClientEvent::Warning {
|
||||||
|
kind: WarningKind::UntrackedReply,
|
||||||
|
message: format!(
|
||||||
|
"dropped reply with untracked seq {wire_seq} (no pending or completed entry)"
|
||||||
|
),
|
||||||
|
at: now,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_from_peer(&mut self, token: u64, now: ClientTimestamp, events: &mut Vec<ClientEvent>) {
|
||||||
|
self.phase = ClientPhase::Closed {
|
||||||
|
source: CloseSource::Peer,
|
||||||
|
};
|
||||||
|
if let Some(session) = self.session.as_mut() {
|
||||||
|
session.timed_out.clear();
|
||||||
|
}
|
||||||
|
self.session = None;
|
||||||
|
events.push(ClientEvent::SessionClosed {
|
||||||
|
remote: self.remote,
|
||||||
|
token,
|
||||||
|
at: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn recv_buffer_size(
|
||||||
|
has_hmac: bool,
|
||||||
|
negotiated: Option<&NegotiatedParams>,
|
||||||
|
) -> Result<usize, ClientError> {
|
||||||
|
Ok(match negotiated {
|
||||||
|
Some(negotiated) => echo_packet_len(has_hmac, &negotiated.params)?
|
||||||
|
.saturating_add(1)
|
||||||
|
.max(MIN_RECV_BUFFER_SIZE),
|
||||||
|
None => MIN_RECV_BUFFER_SIZE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn params_from_config(config: &ClientConfig) -> Result<Params, ClientError> {
|
||||||
|
validate_protocol_config(config)?;
|
||||||
|
Ok(Params {
|
||||||
|
protocol_version: PROTOCOL_VERSION,
|
||||||
|
duration_ns: match config.duration {
|
||||||
|
Some(duration) => config_duration_to_ns("duration", duration)?,
|
||||||
|
None => 0,
|
||||||
|
},
|
||||||
|
interval_ns: config_duration_to_ns("interval", config.interval)?,
|
||||||
|
length: i64::from(config.length),
|
||||||
|
received_stats: config.received_stats,
|
||||||
|
stamp_at: config.stamp_at,
|
||||||
|
clock: config.clock,
|
||||||
|
dscp: i64::from(config.dscp),
|
||||||
|
server_fill: config.server_fill.clone().map(|value| ServerFill { value }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_highest_received(highest_received_seq: &mut Option<u32>, wire_seq: u32) {
|
||||||
|
*highest_received_seq = Some(highest_received_seq.map_or(wire_seq, |h| {
|
||||||
|
if sequence_is_after(wire_seq, h) {
|
||||||
|
wire_seq
|
||||||
|
} else {
|
||||||
|
h
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_probe_deadline(
|
||||||
|
start: Instant,
|
||||||
|
interval_ns: u64,
|
||||||
|
packets_sent: u64,
|
||||||
|
) -> Result<Instant, ClientError> {
|
||||||
|
let offset_ns = interval_ns
|
||||||
|
.checked_mul(packets_sent)
|
||||||
|
.ok_or(ClientError::DurationOverflow)?;
|
||||||
|
start
|
||||||
|
.checked_add(Duration::from_nanos(offset_ns))
|
||||||
|
.ok_or(ClientError::DurationOverflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sequence_is_after(candidate: u32, current: u32) -> bool {
|
||||||
|
candidate != current && candidate.wrapping_sub(current) < (1 << 31)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sequence_is_before(candidate: u32, current: u32) -> bool {
|
||||||
|
current != candidate && current.wrapping_sub(candidate) < (1 << 31)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instant_abs_diff(left: Instant, right: Instant) -> Duration {
|
||||||
|
left.checked_duration_since(right)
|
||||||
|
.or_else(|| right.checked_duration_since(left))
|
||||||
|
.unwrap_or(Duration::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compute_rtt(
|
||||||
|
sent_at: &ClientTimestamp,
|
||||||
|
received_at: &ClientTimestamp,
|
||||||
|
ts: &TimestampFields,
|
||||||
|
) -> RttSample {
|
||||||
|
let raw = received_at
|
||||||
|
.mono
|
||||||
|
.checked_duration_since(sent_at.mono)
|
||||||
|
.unwrap_or(Duration::ZERO);
|
||||||
|
|
||||||
|
let server_processing = compute_server_processing(ts);
|
||||||
|
|
||||||
|
let adjusted = server_processing
|
||||||
|
.map(|sp| SignedDuration::from_nanos(duration_ns_i128(raw) - duration_ns_i128(sp)));
|
||||||
|
let effective = adjusted.unwrap_or_else(|| SignedDuration::from_duration(raw));
|
||||||
|
|
||||||
|
RttSample {
|
||||||
|
raw,
|
||||||
|
adjusted,
|
||||||
|
effective,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duration_ns_i128(duration: Duration) -> i128 {
|
||||||
|
i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_server_processing(ts: &TimestampFields) -> Option<Duration> {
|
||||||
|
if let (Some(recv_mono), Some(send_mono)) = (ts.recv_mono, ts.send_mono) {
|
||||||
|
let diff = send_mono.checked_sub(recv_mono)?;
|
||||||
|
return Some(Duration::from_nanos(u64::try_from(diff).ok()?));
|
||||||
|
}
|
||||||
|
if let (Some(recv_wall), Some(send_wall)) = (ts.recv_wall, ts.send_wall) {
|
||||||
|
let diff = send_wall.checked_sub(recv_wall)?;
|
||||||
|
return Some(Duration::from_nanos(u64::try_from(diff).ok()?));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_server_timing(ts: &TimestampFields) -> Option<ServerTiming> {
|
||||||
|
if ts.recv_wall.is_none()
|
||||||
|
&& ts.recv_mono.is_none()
|
||||||
|
&& ts.send_wall.is_none()
|
||||||
|
&& ts.send_mono.is_none()
|
||||||
|
&& ts.midpoint_wall.is_none()
|
||||||
|
&& ts.midpoint_mono.is_none()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(ServerTiming {
|
||||||
|
receive_wall_ns: ts.recv_wall,
|
||||||
|
receive_mono_ns: ts.recv_mono,
|
||||||
|
send_wall_ns: ts.send_wall,
|
||||||
|
send_mono_ns: ts.send_mono,
|
||||||
|
midpoint_wall_ns: ts.midpoint_wall,
|
||||||
|
midpoint_mono_ns: ts.midpoint_mono,
|
||||||
|
processing: compute_server_processing(ts),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compute_one_way(
|
||||||
|
sent_at: &ClientTimestamp,
|
||||||
|
received_at: &ClientTimestamp,
|
||||||
|
ts: &TimestampFields,
|
||||||
|
) -> Option<OneWayDelaySample> {
|
||||||
|
let server_recv_wall = ts.recv_wall.or(ts.midpoint_wall);
|
||||||
|
let server_send_wall = ts.send_wall.or(ts.midpoint_wall);
|
||||||
|
|
||||||
|
let client_send_ns = unix_epoch_ns_i64(sent_at.wall);
|
||||||
|
let client_recv_ns = unix_epoch_ns_i64(received_at.wall);
|
||||||
|
|
||||||
|
let c2s = server_recv_wall
|
||||||
|
.zip(client_send_ns)
|
||||||
|
.and_then(|(srv, cli)| srv.checked_sub(cli))
|
||||||
|
.map(|d| SignedDuration::from_nanos(i128::from(d)));
|
||||||
|
let s2c = client_recv_ns
|
||||||
|
.zip(server_send_wall)
|
||||||
|
.and_then(|(cli, srv)| cli.checked_sub(srv))
|
||||||
|
.map(|d| SignedDuration::from_nanos(i128::from(d)));
|
||||||
|
|
||||||
|
if c2s.is_none() && s2c.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(OneWayDelaySample {
|
||||||
|
client_to_server: c2s,
|
||||||
|
server_to_client: s2c,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unix_epoch_ns_i64(time: SystemTime) -> Option<i64> {
|
||||||
|
time.duration_since(UNIX_EPOCH)
|
||||||
|
.ok()
|
||||||
|
.and_then(|duration| i64::try_from(duration.as_nanos()).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_received_stats(reply: &EchoReply) -> Option<ReceivedStatsSample> {
|
||||||
|
if reply.recv_count.is_none() && reply.recv_window.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(ReceivedStatsSample {
|
||||||
|
count: reply.recv_count,
|
||||||
|
window: reply.recv_window,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_protocol_config(config: &ClientConfig) -> Result<(), ClientError> {
|
||||||
|
if config.duration == Some(Duration::ZERO) {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: "duration must be greater than zero; use None for continuous mode".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if config.interval == Duration::ZERO {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: "interval must be greater than zero".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if config.dscp > MAX_DSCP_CODEPOINT {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: format!("dscp must be <= {MAX_DSCP_CODEPOINT}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if config.length > MAX_UDP_PAYLOAD_LENGTH {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: format!("packet length must be <= {MAX_UDP_PAYLOAD_LENGTH}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(fill) = &config.server_fill {
|
||||||
|
let len = fill.len();
|
||||||
|
if len == 0 {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: "server_fill must not be empty".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if len > MAX_SERVER_FILL_BYTES {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: format!("server_fill must be <= {MAX_SERVER_FILL_BYTES} bytes, got {len}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_duration_to_ns(field: &str, duration: Duration) -> Result<i64, ClientError> {
|
||||||
|
i64::try_from(duration.as_nanos()).map_err(|_| ClientError::InvalidConfig {
|
||||||
|
reason: format!("{field} is too large to encode as nanoseconds"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn negotiated_end_mono(start: Instant, duration_ns: i64) -> Result<Instant, ClientError> {
|
||||||
|
debug_assert!(duration_ns > 0);
|
||||||
|
let duration_ns = u64::try_from(duration_ns).expect("validated positive negotiated duration");
|
||||||
|
start
|
||||||
|
.checked_add(Duration::from_nanos(duration_ns))
|
||||||
|
.ok_or_else(|| ClientError::NegotiationRejected {
|
||||||
|
reason: "duration is too large to schedule".to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -99,6 +99,44 @@ pub(crate) fn connect_udp_socket(
|
|||||||
Ok(socket)
|
Ok(socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bind_unconnected_udp_socket(
|
||||||
|
config: &SocketConfig,
|
||||||
|
family_remote: SocketAddr,
|
||||||
|
) -> Result<UdpSocket, ClientError> {
|
||||||
|
let domain = if family_remote.is_ipv4() {
|
||||||
|
Domain::IPV4
|
||||||
|
} else {
|
||||||
|
Domain::IPV6
|
||||||
|
};
|
||||||
|
let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
|
||||||
|
|
||||||
|
if config.ipv6_only && family_remote.is_ipv6() {
|
||||||
|
socket.set_only_v6(true)?;
|
||||||
|
}
|
||||||
|
let bind_addr = config.bind_addr.unwrap_or_else(|| {
|
||||||
|
if family_remote.is_ipv4() {
|
||||||
|
SocketAddr::from(([0, 0, 0, 0], 0))
|
||||||
|
} else {
|
||||||
|
SocketAddr::from(([0_u16; 8], 0))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.bind(&bind_addr.into())?;
|
||||||
|
|
||||||
|
let socket: UdpSocket = socket.into();
|
||||||
|
configure_receive_metadata(&socket, family_remote).map_err(|source| {
|
||||||
|
ClientError::SocketOption {
|
||||||
|
operation: "enable receive metadata",
|
||||||
|
remote: family_remote,
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if let Some(ttl) = config.ttl {
|
||||||
|
apply_ttl_to_socket(&socket, family_remote, ttl)?;
|
||||||
|
}
|
||||||
|
socket.set_read_timeout(config.recv_timeout)?;
|
||||||
|
Ok(socket)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in new issue
Block a user