subetha_cxc/udp_bridge.rs
1//! Sens-O-Matic bridge: ordered, lossless item delivery over
2//! [`std::net::UdpSocket`] with no TLS and no async runtime.
3//!
4//! Sens-O-Matic is the reliable-UDP FEC transport - a sighted,
5//! forward-correcting alternative to a blind, reactive ARQ stack. It
6//! *senses* the channel (in-band loss, one-way-delay trend, radio link
7//! stats) and *corrects ahead* (Cauchy Reed-Solomon FEC first, ARQ only
8//! as the floor), named for the Sub-Etha Sens-O-Matic that detects
9//! Sub-Etha signals. The protocol coding lives in [`crate::reliable_udp`];
10//! this module is its socket layer. [`SensOMaticSender`] /
11//! [`SensOMaticReceiver`] are the public names for the bridge pair.
12//!
13//! This is the socket layer over [`crate::reliable_udp`]. It ships
14//! byte-slice items from one endpoint to another with FEC-primary /
15//! ARQ-fallback reliability and an automatic parity rate. It depends
16//! only on `std` - no tokio, no quinn, no rustls - so a trusted-network
17//! bridge that wants UDP's properties without encryption pays nothing
18//! for a TLS stack it does not use.
19//!
20//! [`ReliableUdpSender`] stages items into FEC blocks and answers ARQ
21//! retransmit requests; [`ReliableUdpReceiver`] reassembles blocks,
22//! FEC-recovers losses, delivers items in order, and feeds ACK / NAK /
23//! loss reports back. The receiver socket parks on a read timeout (zero
24//! idle CPU; the timeout also drives tail-ARQ), and the sender socket is
25//! non-blocking so item throughput never waits on feedback.
26
27use std::collections::{BTreeMap, HashMap, VecDeque};
28use std::io;
29use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
30use std::sync::Arc;
31use std::time::{Duration, Instant};
32
33use crate::control_table::ControlTable;
34use crate::fusion::{FusionPolicy, ImmediateUpConservativeDown, SensorSnapshot};
35use crate::interleave::Interleaver;
36use crate::control_frame::{
37 decode_control, encode_control, is_control, AckFrame, ControlPacket, LinkFrame, LossAcctFrame,
38 LossFrame, NakFrame, PathFrame, PmtuFrame, RingFrame, TimingFrame,
39};
40use crate::link_sensor::{platform_sensor, LinkClass, LinkSensor};
41use crate::net_events::NetEventObserver;
42use crate::path_model_sensor::PathModel;
43use crate::path_sensor::PathSensor;
44use crate::rtt_shape_sensor::RttShape;
45use crate::reliable_udp::{
46 datagram_epoch, is_outer_datagram, Decoder, Encoder, Feedback, NAK_NONE,
47};
48
49/// Receive-buffer size for an inbound CONTROL datagram. Generous: a control
50/// packet carrying every frame is well under this, and over-sizing costs only
51/// stack.
52const CONTROL_RECV_BUF: usize = 256;
53
54/// Extract the ack / NAK / loss frames of a decoded control packet into the
55/// sender-side [`Feedback`] its controller already consumes. Absent frames
56/// fall back to neutral defaults (no ack, no NAK, zero loss).
57fn feedback_from_control(cp: &ControlPacket) -> Feedback {
58 let (nak_block, nak_mask) = cp.nak.map(|n| (n.block, n.mask)).unwrap_or((NAK_NONE, 0));
59 let loss = cp.loss.unwrap_or_default();
60 Feedback {
61 ack_through: cp.ack.map(|a| a.ack_through).unwrap_or(0),
62 nak_block,
63 nak_mask,
64 loss_x255: loss.loss_x255,
65 burstiness_x255: loss.burstiness_x255,
66 owd_trend_class: loss.owd_trend_class,
67 loss_class: loss.loss_class,
68 }
69}
70
71
72/// The Sens-O-Matic sender carrying the **block Reed-Solomon** erasure code -
73/// the original, MDS, fixed-parity, std-only code. Sens-O-Matic is the protocol
74/// (the reliable FEC-UDP transport); the erasure code is its swappable detail,
75/// like a cipher suite. The other code, sliding-window RLC, is
76/// [`crate::sens_rlc::SensOMaticRlcSender`]. A branded alias for
77/// [`ReliableUdpSender`].
78pub type SensOMaticRsSender = ReliableUdpSender;
79
80/// The Sens-O-Matic receiver for the block Reed-Solomon code. RLC counterpart:
81/// [`crate::sens_rlc::SensOMaticRlcReceiver`]. A branded alias for
82/// [`ReliableUdpReceiver`].
83pub type SensOMaticRsReceiver = ReliableUdpReceiver;
84
85/// Bare Sens-O-Matic aliases default to the Reed-Solomon code (the original).
86/// Spell the code explicitly with [`SensOMaticRsSender`] /
87/// [`crate::sens_rlc::SensOMaticRlcSender`] when it matters.
88pub type SensOMaticSender = ReliableUdpSender;
89/// Bare Sens-O-Matic receiver alias (Reed-Solomon code); see [`SensOMaticSender`].
90pub type SensOMaticReceiver = ReliableUdpReceiver;
91
92/// How long a session challenge waits for its answer. A restarted peer
93/// answers within a round trip; a forged epoch from an address that
94/// cannot receive never does.
95const SESSION_CHALLENGE_TIMEOUT: Duration = Duration::from_millis(500);
96
97/// How long the receiver's socket stays bound to one peer after that peer
98/// goes silent.
99///
100/// A connected UDP socket accepts datagrams from its peer alone, which is
101/// what buys the batched and GRO receive paths - and what makes a peer
102/// that restarts on a fresh ephemeral port unhearable, since the kernel
103/// discards it before any of this code runs. Silence past this mark
104/// dissolves the association so the receiver hears the world again; a
105/// validated session re-connects and the fast paths resume.
106const PEER_SILENCE_TIMEOUT: Duration = Duration::from_secs(2);
107
108/// The address a socket is connected to in order to have no peer.
109const UNSPECIFIED_PEER: SocketAddr = SocketAddr::new(
110 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
111 0,
112);
113
114/// Largest datagram the receiver will read. A shard is `DATA_HEADER +
115/// shard_len` bytes; this bounds `shard_len` to a typical MTU payload.
116const RECV_BUF: usize = 2048;
117
118/// The `vlen` argument type of `sendmmsg` / `recvmmsg`. Linux types it as
119/// `unsigned int`; the BSDs type it as `size_t`. Aliasing keeps the one
120/// scatter-gather code path compiling on both.
121#[cfg(target_os = "linux")]
122type MmsgLen = libc::c_uint;
123#[cfg(target_os = "freebsd")]
124type MmsgLen = usize;
125
126/// Minimum spacing between NAKs for the SAME block. Feedback is emitted
127/// on every poll, so without this a single lost block draws a NAK on
128/// every packet and the sender retransmits it hundreds of times per
129/// round-trip. One re-request per this interval is roughly one per RTT
130/// on a LAN / Wi-Fi link.
131const NAK_COOLDOWN: Duration = Duration::from_millis(12);
132
133/// Target socket buffer size (receive and send). The flow window keeps
134/// ~256 blocks of `k + r` shards in flight (~1 MiB); a buffer this size
135/// holds that backlog so a fast clean link does not overflow the kernel
136/// buffer and manufacture loss that would keep FEC needlessly armed. The
137/// OS clamps the request to its configured maximum.
138const SOCK_BUF_BYTES: usize = 8 << 20;
139
140/// Size `sock`'s receive and send buffers to [`SOCK_BUF_BYTES`]. Best-effort:
141/// a kernel that refuses or clamps the request just keeps a smaller buffer.
142fn size_socket_buffers(sock: &UdpSocket) {
143 let s = socket2::SockRef::from(sock);
144 s.set_recv_buffer_size(SOCK_BUF_BYTES).ok();
145 s.set_send_buffer_size(SOCK_BUF_BYTES).ok();
146}
147
148/// Minimum spacing between plain ACK feedback packets. The ack frontier
149/// is cumulative, so it does not need a syscall on every datagram - a
150/// NAK, a timeout drive, or this interval elapsing each force one.
151const ACK_INTERVAL: Duration = Duration::from_millis(1);
152
153/// Cap on NAKs emitted in one poll cycle. The receiver re-requests every
154/// gap it is holding in a single round-trip (selective NAK) instead of
155/// chasing them serially, but a burst of loss can leave many gaps at
156/// once; this bounds the feedback burst per cycle and the rest are picked
157/// up on the next poll (every few ms), so recovery stays parallel without
158/// a feedback storm.
159const MAX_NAKS_PER_CYCLE: usize = 64;
160
161/// How often the sender emits a heartbeat (timestamp + ring digest).
162const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(20);
163
164/// WBest active probe (item 13). A round is emitted this often; it is low
165/// intrusion (a few dozen padded packets every couple of seconds), so it does
166/// not perturb the transfer it measures.
167const BW_PROBE_INTERVAL: Duration = Duration::from_secs(2);
168/// Packet pairs in stage 1 (effective-capacity median) and packets in the
169/// stage-2 train (available-bandwidth measurement).
170const BW_PROBE_PAIRS: u8 = 8;
171const BW_PROBE_TRAIN: u8 = 12;
172/// On-wire size of each probe datagram. Large enough that the bottleneck
173/// serialization dispersion is tens of microseconds (measurable against the
174/// clock and jitter), the size the receiver's estimator assumes.
175const BW_PROBE_BYTES: usize = 1400;
176
177/// Trace mini-traceroute (item 14). A sweep of probes at IP TTL 1..=`MAX_TRACE_HOPS`
178/// is emitted this often; each expired probe draws an ICMP TimeExceeded the
179/// sender reads off its error queue for the per-hop router and RTT. The cadence
180/// only drives the Linux error-queue path, so it is dead on other targets.
181#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
182const TRACE_INTERVAL: Duration = Duration::from_secs(3);
183const MAX_TRACE_HOPS: u8 = 8;
184
185/// Sprout forecast tick (item 16): the receiver integrates arrivals over this
186/// interval into one rate observation, the "next tick" the forecast bounds.
187const FORECAST_TICK: Duration = Duration::from_millis(50);
188/// Headroom above the forecast the predictive window cap allows, so the sender
189/// keeps probing the link (the forecast can climb back) and the cap bites only
190/// on a real dip - a forecast below `BtlBw / FORECAST_HEADROOM`.
191const FORECAST_HEADROOM: f64 = 2.0;
192
193/// How often the sender polls its platform link sensor (slow cadence,
194/// never per packet).
195const LINK_SAMPLE_INTERVAL: Duration = Duration::from_millis(200);
196
197/// Floor the bufferbloat pacer will not shrink the flow window below, so a
198/// transient BDP under-estimate cannot choke the pipe to a standstill.
199const MIN_PACED_WINDOW: u32 = 4;
200
201/// Target self-induced queue delay (ms) the LEDBAT pacer holds the window at:
202/// enough standing queue to keep the bottleneck busy, little enough that the
203/// added latency is small. RFC 6817 uses 100 ms for background bulk; a reliable
204/// real-time transport wants the queue much shorter.
205const PACE_TARGET_MS: f32 = 10.0;
206
207/// Minimum spacing between pacer adjustments when `RTprop` is not yet known (1
208/// ms). The queue responds a round trip after a window change, so the pacer
209/// adjusts at most once per round trip; before the first RTT sample it falls
210/// back to this floor.
211const MIN_PACE_INTERVAL_US: u64 = 1000;
212
213/// Multiple of the smoothed RTT after which TOTAL silence (no feedback of any
214/// kind) marks the link dead. Several round trips with nothing back is a
215/// liveness failure, not jitter.
216const DEAD_RTT_MULTIPLE: u64 = 8;
217
218/// Floor on the dead-link timeout (250 ms): a healthy link returns feedback
219/// every few ms, so a quarter second of total silence is dead regardless of a
220/// tiny RTT. The dead timeout is `max(DEAD_RTT_MULTIPLE * SRTT, this)`, and the
221/// probe cadence while dead reuses it.
222const DEAD_FLOOR_US: u64 = 250_000;
223
224/// Floor on the rate the recovery resend is paced at (1 MB/s = 8 Mbit/s) when
225/// no BtlBw estimate is available yet, so recovery still makes progress on a
226/// link whose capacity was never measured.
227const MIN_RECOVERY_BYTES_PER_S: u64 = 1_000_000;
228
229/// Token-bucket depth for the paced recovery resend (8 KB ~ a few datagrams):
230/// large enough to keep the pipe fed, small enough that the resend stays paced
231/// at BtlBw rather than bursting.
232const RECOVERY_BUCKET_BYTES: f64 = 8192.0;
233
234/// Round trips of grace after the recovery resend drains during which the pacer
235/// still holds (lets the recovery's queue clear before normal control resumes).
236const RECOVERY_GRACE_RTTS: u64 = 4;
237
238/// Wi-Fi-shape confidence above which the RTT-bimodality fingerprint fills the
239/// link class as Wi-Fi when the OS wireless read is unavailable. A clear
240/// margin above the bimodality threshold, so borderline shapes do not flip it.
241const WIFI_SHAPE_CONFIDENCE: f32 = 0.15;
242
243/// Sender half of the reliable-UDP bridge.
244pub struct ReliableUdpSender {
245 sock: crate::dgram::DgramSock,
246 enc: Encoder,
247 interleaver: Interleaver,
248 control: Arc<ControlTable>,
249 /// Fusion controller: maps receiver-reported sensors to coding knobs.
250 fusion: Box<dyn FusionPolicy + Send>,
251 /// Platform link sensor (radio / interface stats), polled slowly.
252 link_sensor: Box<dyn LinkSensor + Send>,
253 /// Last link-stress reading (0..1), fed forward into fusion.
254 link_stress: f32,
255 /// Last link class and a normalized quality from the link sensor, reported
256 /// to the peer in the `Link` frame. `class_shift` spikes to 1.0 on a class
257 /// change (a handoff - Wi-Fi to cellular, a wired uplink dropping to Wi-Fi)
258 /// and decays, pre-arming protection like a hop-count shift does.
259 link_class: LinkClass,
260 link_quality: u8,
261 class_shift: f32,
262 /// Last first-hop PHY rate (kbit/s) and normalized MCS from the link sensor.
263 /// The PHY rate is `nominal` for mesh-hop detection (the rate one Wi-Fi hop
264 /// can carry); the MCS gates it (a healthy first hop means a low end-to-end
265 /// `BtlBw` is a downstream backhaul hop, not a weak local radio).
266 link_phy_kbps: u32,
267 link_mcs_norm: f32,
268 /// EWMA share of recent loss the peer classed congestion (0..1), from the
269 /// `loss_class` it echoes. Congestion loss drives parity up broadly; a
270 /// wireless drop is recovered locally without inflating effective loss.
271 congestion_fraction: f32,
272 /// Bidirectional control-plane loss accounting. `ctrl_out` counts heartbeat
273 /// control packets sent, `ctrl_recv` counts feedback control packets
274 /// received, and `peer_seq` is the highest `seq` the receiver has reported
275 /// (how many feedback packets it sent). `rev_loss` is the share of the
276 /// receiver's feedback we missed - reverse-path loss that stalls ARQ,
277 /// distinct from the forward-path data loss the receiver measures.
278 ctrl_out: u32,
279 ctrl_recv: u32,
280 peer_seq: u32,
281 rev_loss: f32,
282 /// The forward-loss fraction (0..=1) the receiver last fed back, stored so
283 /// the unified endpoint can read it to drive the RS -> RLC code switch.
284 last_fwd_loss: f32,
285 /// Path sensor fed by the peer's echoed TTL / ECN observations: hop-count
286 /// shifts and ECN congestion, both feed-forward predictors of loss.
287 path_sensor: PathSensor,
288 /// Active OS path-event observer: a background netlink / route watcher that
289 /// spikes a path shift the instant the kernel announces a route, carrier,
290 /// or MTU change - ahead of any loss, and ahead of the passive hop-count
291 /// shift the `path_sensor` derives a round trip later. Fused as a third
292 /// `path_shift` source. Its local egress MTU is reported to the peer in a
293 /// `Pmtu` frame.
294 net_events: NetEventObserver,
295 /// The peer's last reported path MTU (from its `Pmtu` frame), and a decaying
296 /// shift that spikes when that MTU drops - a peer-side handoff (a lower-MTU
297 /// link engaging at the other end) is a path event this end should pre-arm
298 /// for too. 0 = no report yet.
299 peer_pmtu: u16,
300 peer_pmtu_shift: f32,
301 /// Peak event-driven path shift reached over the run (the OS-observer spike
302 /// or a peer-MTU-drop spike). The instantaneous shift decays within a few
303 /// seconds of the event, so this peak-hold is what makes a mid-transfer
304 /// path event visible in the end-of-run telemetry.
305 net_event_shift_peak: f32,
306 /// BBR-style passive path model: bottleneck bandwidth, RTprop, and BDP,
307 /// recovered from the ACK stream. Sizes the flow window and informs the
308 /// pacer; it does not feed parity directly.
309 path_model: PathModel,
310 /// RTT-distribution shape fingerprint: a bimodal RTT (a fast first-transmit
311 /// cluster and a slow retried cluster) means a Wi-Fi hop on the path, so the
312 /// link class can be filled even when the local OS wireless read is
313 /// unavailable (a wired host whose peer is on Wi-Fi).
314 rtt_shape: RttShape,
315 /// Per-block first-send time (block id, time_us) in send order, so an ACK
316 /// that delivers a block yields its round-trip time. Pruned below the ack
317 /// frontier each feedback, so it stays bounded by the in-flight window.
318 block_send_us: VecDeque<(u32, u64)>,
319 /// The full (un-paced) flow window captured at construction; the bufferbloat
320 /// pacer only ever clamps the encoder's window DOWN from this toward the BDP
321 /// to drain a self-induced queue, and restores it when the queue clears.
322 flow_window_max: u32,
323 /// Whether the bufferbloat pacer is active. On by default; an A/B harness
324 /// can disable it to measure the un-paced baseline.
325 pacing_enabled: bool,
326 /// The LEDBAT pacer's flow window as a real number (the integer encoder
327 /// window is its rounding). Starts at the full window and is nudged toward
328 /// the size that holds the queue at [`PACE_TARGET_MS`].
329 paced_window: f32,
330 /// Time of the last pacer adjustment (microseconds since `start`); the pacer
331 /// adjusts at most once per round trip.
332 last_pace_us: u64,
333 /// Link-liveness state. `last_feedback_at` is when the sender last received
334 /// ANY feedback; when the silence exceeds a PTO derived from the smoothed
335 /// RTT the link is declared dead. While dead the producer is already held by
336 /// flow-control backpressure (the window cannot advance with no ACKs); the
337 /// sender adds a periodic probe (a retransmit of the oldest unacked block)
338 /// to both detect recovery and pre-position the stalled frontier. On the
339 /// first feedback after a dead spell it proactively bursts the whole unacked
340 /// window oldest-first, rather than waiting a round trip per NAK.
341 last_feedback_at: Instant,
342 link_dead: bool,
343 last_probe_at: Instant,
344 /// Whether proactive burst-recovery is enabled (the A/B baseline disables it
345 /// to fall back to reactive NAK recovery).
346 proactive_recovery: bool,
347 /// Telemetry: dead spells detected, probes sent, blocks proactively
348 /// retransmitted on recovery.
349 dead_episodes: u64,
350 probes_sent: u64,
351 recovered_blocks: u64,
352 /// Proactive-recovery resend queue (datagrams, oldest block first) and its
353 /// token bucket. On recovery the whole still-unacked gap is enqueued here
354 /// and drained at the item-6 BtlBw rate - the rate that fills the pipe
355 /// without overflowing the buffer - so the recovery cooperates with the
356 /// bufferbloat pacer instead of dumping a burst that trips it.
357 recovery_dgrams: VecDeque<Vec<u8>>,
358 recovery_tokens: f64,
359 last_recovery_us: u64,
360 /// Until this time (microseconds since `start`) the bufferbloat pacer holds
361 /// its window instead of clamping: we KNOW a recovery resend is in flight,
362 /// so the queue it briefly adds is an expected, intentional transient, not
363 /// steady-state bloat. Without this the recovery would still throttle the
364 /// window it just refilled. Extended while the resend drains, plus a grace
365 /// of a few round trips for the queue to clear.
366 recovery_grace_until_us: u64,
367 /// Recovery-interval measurement: when a dead spell ends, `recovery_target`
368 /// is the highest block id sent so far and `recovery_started_us` the time;
369 /// when the ack frontier reaches that target the whole pre-outage backlog is
370 /// re-delivered and `recovery_interval_us` records how long it took. This
371 /// isolates the recovery speed (proactive resend vs reactive NAK learning)
372 /// from the noisy total transfer time. `recovery_target == 0` means idle.
373 recovery_target: u32,
374 recovery_started_us: u64,
375 recovery_interval_us: u64,
376 /// Monotonic clock origin for heartbeat timestamps.
377 start: Instant,
378 /// When the last heartbeat went out.
379 last_hb: Instant,
380 /// When the link sensor was last polled.
381 last_link_sample: Instant,
382 /// When the last WBest probe round (item 13) was emitted, and its round id.
383 /// A round is a burst of padded packet-pair probes followed by a packet
384 /// train; the receiver measures their dispersion and reports the available
385 /// bandwidth back, which the sender cross-checks against its passive BtlBw.
386 last_bw_probe: Instant,
387 bw_probe_round: u8,
388 /// The receiver's most recent WBest report (kbit/s): available bandwidth and
389 /// effective capacity. 0 = none yet.
390 avail_bw_kbps: u64,
391 wbest_capacity_kbps: u64,
392 /// Trace mini-traceroute (item 14): the connected peer, the probe cadence /
393 /// round, the per-TTL send time (for the RTT), the discovered hops, and the
394 /// forward/reverse path-asymmetry tracker. The probe-emission fields only
395 /// drive the Linux error-queue path, so they are dead on other targets.
396 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
397 trace_peer: SocketAddr,
398 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
399 last_trace: Instant,
400 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
401 trace_round: u8,
402 #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
403 trace_send_us: Vec<u64>,
404 trace_hops: Vec<crate::trace_sensor::TraceHop>,
405 asym: crate::trace_sensor::PathAsymmetry,
406 /// AccECN (item 15): the graded CE rate the peer's cumulative CE / ECT counts
407 /// imply (`ce_count / ect_count`).
408 ce_rate: f32,
409 /// Sprout forecast (item 16): the peer's 5th-percentile next-tick deliverable
410 /// rate (bytes/s), so the sender pre-sizes its window ahead of a dip.
411 forecast_bps: u64,
412 /// LEO cadence (item 17): the peer's detected handover period (seconds), its
413 /// confidence, and the seconds to the next predicted spike. When a spike is
414 /// imminent the sender pre-arms FEC one cycle ahead.
415 leo_period_s: f32,
416 leo_conf: f32,
417 leo_secs_to_spike: f32,
418}
419
420/// ABI of the `WSASendMsg` extension entry point (Windows). It is not a
421/// direct `ws2_32` export, so it is fetched once via
422/// `WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER)`.
423#[cfg(target_os = "windows")]
424type LpfnWsaSendMsg = unsafe extern "system" fn(
425 usize,
426 *const windows_sys::Win32::Networking::WinSock::WSAMSG,
427 u32,
428 *mut u32,
429 *mut core::ffi::c_void,
430 *const core::ffi::c_void,
431) -> i32;
432
433/// Process-wide cache of the `WSASendMsg` pointer. `None` means the load
434/// failed, so USO is treated as unsupported and the caller falls back to
435/// per-datagram sends.
436#[cfg(target_os = "windows")]
437static WSASENDMSG_PTR: std::sync::OnceLock<Option<LpfnWsaSendMsg>> =
438 std::sync::OnceLock::new();
439
440/// Fetch (and cache) the `WSASendMsg` extension function pointer using the
441/// given socket. The pointer is valid for every socket in the process, so
442/// the first successful load is reused for the program's lifetime.
443#[cfg(target_os = "windows")]
444fn load_wsasendmsg(sock: usize) -> Option<LpfnWsaSendMsg> {
445 *WSASENDMSG_PTR.get_or_init(|| {
446 use windows_sys::Win32::Networking::WinSock::WSAIoctl;
447 const SIO_GET_EXTENSION_FUNCTION_POINTER: u32 = 0xC800_0006;
448 // WSAID_WSASENDMSG = {a441e712-754f-43ca-84a7-0dee44cf606d}
449 let guid = windows_sys::core::GUID {
450 data1: 0xa441_e712,
451 data2: 0x754f,
452 data3: 0x43ca,
453 data4: [0x84, 0xa7, 0x0d, 0xee, 0x44, 0xcf, 0x60, 0x6d],
454 };
455 let mut func: usize = 0;
456 let mut bytes: u32 = 0;
457 // SAFETY: WSAIoctl on a valid connected socket; guid/func/bytes
458 // outlive the call; the out buffer is exactly usize-sized.
459 let rc = unsafe {
460 WSAIoctl(
461 sock,
462 SIO_GET_EXTENSION_FUNCTION_POINTER,
463 &guid as *const _ as *const core::ffi::c_void,
464 size_of::<windows_sys::core::GUID>() as u32,
465 &mut func as *mut usize as *mut core::ffi::c_void,
466 size_of::<usize>() as u32,
467 &mut bytes,
468 std::ptr::null_mut(),
469 None,
470 )
471 };
472 if rc != 0 || func == 0 {
473 None
474 } else {
475 let p = func as *const core::ffi::c_void;
476 // SAFETY: WSAIoctl populated `func` with the WSASendMsg entry
477 // point, whose ABI matches `LpfnWsaSendMsg`.
478 Some(unsafe { std::mem::transmute::<*const core::ffi::c_void, LpfnWsaSendMsg>(p) })
479 }
480 })
481}
482
483/// ABI of the `WSARecvMsg` extension entry point (Windows). Like
484/// `WSASendMsg` it is not a direct `ws2_32` export, so it is fetched once
485/// via `WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER)`.
486#[cfg(target_os = "windows")]
487type LpfnWsaRecvMsg = unsafe extern "system" fn(
488 usize,
489 *mut windows_sys::Win32::Networking::WinSock::WSAMSG,
490 *mut u32,
491 *mut core::ffi::c_void,
492 *const core::ffi::c_void,
493) -> i32;
494
495/// Process-wide cache of the `WSARecvMsg` pointer. `None` means the load
496/// failed, so the receiver falls back to a plain `recv` with no TTL / ECN
497/// cmsg.
498#[cfg(target_os = "windows")]
499static WSARECVMSG_PTR: std::sync::OnceLock<Option<LpfnWsaRecvMsg>> =
500 std::sync::OnceLock::new();
501
502/// Fetch (and cache) the `WSARecvMsg` extension function pointer. Valid for
503/// every socket in the process, so the first successful load is reused for
504/// the program's lifetime. Mirrors [`load_wsasendmsg`].
505#[cfg(target_os = "windows")]
506fn load_wsarecvmsg(sock: usize) -> Option<LpfnWsaRecvMsg> {
507 *WSARECVMSG_PTR.get_or_init(|| {
508 use windows_sys::Win32::Networking::WinSock::WSAIoctl;
509 const SIO_GET_EXTENSION_FUNCTION_POINTER: u32 = 0xC800_0006;
510 // WSAID_WSARECVMSG = {f689d7c8-6f1f-436b-8a53-e54fe351c322}
511 let guid = windows_sys::core::GUID {
512 data1: 0xf689_d7c8,
513 data2: 0x6f1f,
514 data3: 0x436b,
515 data4: [0x8a, 0x53, 0xe5, 0x4f, 0xe3, 0x51, 0xc3, 0x22],
516 };
517 let mut func: usize = 0;
518 let mut bytes: u32 = 0;
519 // SAFETY: WSAIoctl on a valid socket; guid/func/bytes outlive the
520 // call; the out buffer is exactly usize-sized.
521 let rc = unsafe {
522 WSAIoctl(
523 sock,
524 SIO_GET_EXTENSION_FUNCTION_POINTER,
525 &guid as *const _ as *const core::ffi::c_void,
526 size_of::<windows_sys::core::GUID>() as u32,
527 &mut func as *mut usize as *mut core::ffi::c_void,
528 size_of::<usize>() as u32,
529 &mut bytes,
530 std::ptr::null_mut(),
531 None,
532 )
533 };
534 if rc != 0 || func == 0 {
535 None
536 } else {
537 let p = func as *const core::ffi::c_void;
538 // SAFETY: WSAIoctl populated `func` with the WSARecvMsg entry
539 // point, whose ABI matches `LpfnWsaRecvMsg`.
540 Some(unsafe { std::mem::transmute::<*const core::ffi::c_void, LpfnWsaRecvMsg>(p) })
541 }
542 })
543}
544
545/// Whether the USO send path is enabled (default on). `SUBETHA_USO=0`
546/// disables it for the per-datagram A/B baseline. Read once and cached.
547#[cfg(target_os = "windows")]
548fn uso_enabled() -> bool {
549 static EN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
550 *EN.get_or_init(|| std::env::var("SUBETHA_USO").map(|v| v != "0").unwrap_or(true))
551}
552
553/// Count of USO sends the kernel accepted for in-stack segmentation.
554#[cfg(target_os = "windows")]
555static USO_OFFLOAD: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
556/// Count of USO sends the kernel rejected, forcing per-datagram fallback.
557#[cfg(target_os = "windows")]
558static USO_FALLBACK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
559
560/// Process-wide USO telemetry as `(offload_batches, fallback_batches)`. A
561/// nonzero first value means `WSASendMsg` with `UDP_SEND_MSG_SIZE` engaged
562/// in-stack segmentation; a nonzero second means the kernel rejected USO and
563/// the sender fell back to per-datagram sends. Windows-only; `(0, 0)`
564/// everywhere else.
565pub fn uso_stats() -> (u64, u64) {
566 #[cfg(target_os = "windows")]
567 {
568 use std::sync::atomic::Ordering::Relaxed;
569 (USO_OFFLOAD.load(Relaxed), USO_FALLBACK.load(Relaxed))
570 }
571 #[cfg(not(target_os = "windows"))]
572 {
573 (0, 0)
574 }
575}
576
577impl ReliableUdpSender {
578 /// Bind `local` and target `peer`. `k` data shards and an initial
579 /// `r` parity shards per block; `max_item` is the largest item byte
580 /// length. The socket is connected to `peer` and set non-blocking.
581 /// Uses a private default [`ControlTable`] (interleave depth 1 =
582 /// pass-through); use [`bind_with_control`](Self::bind_with_control)
583 /// to share one with a controller.
584 pub fn bind(
585 local: impl ToSocketAddrs,
586 peer: SocketAddr,
587 k: usize,
588 r: usize,
589 max_item: usize,
590 ) -> io::Result<Self> {
591 Self::bind_with_control(local, peer, k, r, max_item, Arc::new(ControlTable::new()))
592 }
593
594 /// Like [`bind`](Self::bind) but shares a [`ControlTable`] with a
595 /// controller, so interleave depth (and, as further knobs are
596 /// wired, parity and coding level) are driven from it at runtime.
597 pub fn bind_with_control(
598 local: impl ToSocketAddrs,
599 peer: SocketAddr,
600 k: usize,
601 r: usize,
602 max_item: usize,
603 control: Arc<ControlTable>,
604 ) -> io::Result<Self> {
605 // The per-block shard bitmap is a u32, so a block can hold at most
606 // MAX_SHARDS (32) shards. k data shards alone must fit (k > MAX_SHARDS
607 // overflows `1 << shard_index`); the encoder caps adaptive parity so
608 // k + r stays within the bound. Reject an out-of-range k loudly here
609 // rather than letting it silently corrupt the bitmap and stall delivery.
610 if !(1..=crate::reliable_udp::MAX_SHARDS).contains(&k) {
611 return Err(io::Error::new(
612 io::ErrorKind::InvalidInput,
613 format!(
614 "RS data-shard count k={k} out of range: need 1 <= k <= {}",
615 crate::reliable_udp::MAX_SHARDS
616 ),
617 ));
618 }
619 let sock = UdpSocket::bind(local)?;
620 sock.connect(peer)?;
621 sock.set_nonblocking(true)?;
622 size_socket_buffers(&sock);
623 // Item 14: turn on the ICMP error queue (so an expired-TTL Trace probe's
624 // TimeExceeded is delivered) and per-packet RX TTL (so the feedback's hop
625 // count gives the reverse-path length for the asymmetry). Linux only.
626 #[cfg(target_os = "linux")]
627 {
628 use std::os::fd::AsRawFd;
629 crate::trace_sensor::enable_icmp_errors(sock.as_raw_fd());
630 }
631 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
632 {
633 enable_ttl_ecn(&sock);
634 // Item 15: mark our data ECN-capable so an AQM marks CE, not drops.
635 set_ect(&sock);
636 }
637 // Wrap as the plain-UDP DgramSock backend AFTER the raw-fd feature setup
638 // above: the standalone RS path keeps the fd (via as_udp) for GRO / TTL
639 // / ECN / connected-send / USO; the unified path swaps in a demux socket.
640 let sock = crate::dgram::DgramSock::from_udp(sock);
641 let depth = control.interleave_depth() as usize;
642 let now = Instant::now();
643 let enc = Encoder::new(k, r, max_item);
644 let flow_window_max = enc.flow_window();
645 Ok(Self {
646 sock,
647 enc,
648 interleaver: Interleaver::new(depth),
649 control,
650 fusion: Box::new(ImmediateUpConservativeDown::new(8)),
651 link_sensor: platform_sensor(None),
652 link_stress: 0.0,
653 link_class: LinkClass::Unknown,
654 link_quality: 0,
655 class_shift: 0.0,
656 link_phy_kbps: 0,
657 link_mcs_norm: 0.0,
658 congestion_fraction: 0.0,
659 ctrl_out: 0,
660 ctrl_recv: 0,
661 peer_seq: 0,
662 rev_loss: 0.0,
663 last_fwd_loss: 0.0,
664 path_sensor: PathSensor::new(),
665 net_events: NetEventObserver::start(None),
666 peer_pmtu: 0,
667 peer_pmtu_shift: 0.0,
668 net_event_shift_peak: 0.0,
669 // Goodput block size: k data shards of `max_item` payload each
670 // (parity and headers are wire overhead, not delivered data).
671 path_model: PathModel::new(k * max_item),
672 rtt_shape: RttShape::new(),
673 block_send_us: VecDeque::new(),
674 flow_window_max,
675 pacing_enabled: true,
676 paced_window: flow_window_max as f32,
677 last_pace_us: 0,
678 last_feedback_at: now,
679 link_dead: false,
680 last_probe_at: now,
681 proactive_recovery: true,
682 dead_episodes: 0,
683 probes_sent: 0,
684 recovered_blocks: 0,
685 recovery_dgrams: VecDeque::new(),
686 recovery_tokens: 0.0,
687 last_recovery_us: 0,
688 recovery_grace_until_us: 0,
689 recovery_target: 0,
690 recovery_started_us: 0,
691 recovery_interval_us: 0,
692 start: now,
693 // Backdated so the very first `send_item` emits a heartbeat (after
694 // one block, before the bottleneck queue fills), letting the
695 // receiver's loss differentiator capture the empty-queue ROTT
696 // baseline. Without this the first heartbeat lands at one interval,
697 // by when a fast-filling queue is already full and the Spike has no
698 // baseline to measure congestion against.
699 last_hb: now.checked_sub(HEARTBEAT_INTERVAL).unwrap_or(now),
700 // Backdated so the very first `maybe_sample_link` reads the
701 // adapter immediately: the link-stress feed-forward must be live
702 // from the first block, not after one sample interval (otherwise
703 // a clean-but-degraded link could drop to Passthrough before the
704 // sensor is ever read).
705 last_link_sample: now.checked_sub(LINK_SAMPLE_INTERVAL).unwrap_or(now),
706 last_bw_probe: now,
707 bw_probe_round: 0,
708 avail_bw_kbps: 0,
709 wbest_capacity_kbps: 0,
710 trace_peer: peer,
711 last_trace: now,
712 trace_round: 0,
713 trace_send_us: vec![0u64; MAX_TRACE_HOPS as usize + 1],
714 trace_hops: Vec::new(),
715 asym: crate::trace_sensor::PathAsymmetry::new(),
716 ce_rate: 0.0,
717 forecast_bps: 0,
718 leo_period_s: 0.0,
719 leo_conf: 0.0,
720 leo_secs_to_spike: 0.0,
721 })
722 }
723
724 /// The current link-stress reading (0..1) from the platform sensor.
725 pub fn link_stress(&self) -> f32 {
726 self.link_stress
727 }
728
729 /// The last `(ttl, ecn, hop_count)` the peer echoed about THIS endpoint's
730 /// packets, or `None` if no `Path` frame has arrived yet. A nonzero TTL
731 /// proves the receiver extracted it from the wire and the control plane
732 /// carried it back. Diagnostics for the path-sensing feed-forward.
733 pub fn path_observation(&self) -> Option<(u8, u8, u8)> {
734 self.path_sensor.last()
735 }
736
737 /// Count of OS path events (route / carrier / MTU changes) the active
738 /// observer has seen. A nonzero value is the durable proof a real path
739 /// event fired - the active observer's headline signal (telemetry).
740 pub fn net_event_count(&self) -> u64 {
741 self.net_events.event_count()
742 }
743
744 /// This endpoint's egress path MTU in bytes (0 = unknown), reported to the
745 /// peer in the `Pmtu` frame (telemetry).
746 pub fn local_pmtu(&self) -> u16 {
747 self.net_events.pmtu().unwrap_or(0)
748 }
749
750 /// The peer's last reported path MTU in bytes (0 = none yet), from its
751 /// `Pmtu` frame (telemetry).
752 pub fn peer_pmtu(&self) -> u16 {
753 self.peer_pmtu
754 }
755
756 /// The current event-driven path-shift contribution: the larger of the OS
757 /// observer's decaying spike and the peer-MTU-drop spike (telemetry).
758 pub fn net_event_shift(&self) -> f32 {
759 self.net_events.path_shift().max(self.peer_pmtu_shift)
760 }
761
762 /// The peak event-driven path shift reached over the run. Unlike the
763 /// instantaneous shift, which decays within a few seconds of the event,
764 /// this holds the spike, so a mid-transfer path event stays visible at the
765 /// end of the run (telemetry).
766 pub fn net_event_shift_peak(&self) -> f32 {
767 self.net_event_shift_peak
768 }
769
770 /// Synthetically fire a path event (the `--sim-path-event` demo path on a
771 /// host where flapping a real interface is impractical). The production
772 /// path is the active OS observer.
773 pub fn inject_path_event(&self) {
774 self.net_events.inject_event();
775 }
776
777 /// Synthetically set this endpoint's egress MTU (a drop also records a path
778 /// event), as a real OS MTU change would. For tests / demos; production
779 /// reads it from the active observer.
780 pub fn inject_pmtu(&self, mtu: u16) {
781 self.net_events.inject_pmtu(mtu);
782 }
783
784 /// The current congestion share (0..=1) of the peer's reported loss, from
785 /// its echoed `loss_class` (Biaz + Spike). High when loss is congestion-
786 /// driven (rising delay), low when it is random wireless loss. Diagnostics
787 /// for the loss differentiator.
788 pub fn congestion_fraction(&self) -> f32 {
789 self.congestion_fraction
790 }
791
792 /// Reverse-path (feedback) loss share (0..=1): the fraction of the
793 /// receiver's feedback control packets this sender missed, from the
794 /// `LossAcct` the receiver echoes. Distinct from the forward-path data loss
795 /// the receiver measures; lost feedback stalls ARQ, so the receiver responds
796 /// by shortening its ACK cadence. Diagnostics.
797 pub fn rev_loss(&self) -> f32 {
798 self.rev_loss
799 }
800
801 /// The platform link-sensor backend in use (diagnostics).
802 pub fn link_backend(&self) -> &'static str {
803 self.link_sensor.backend()
804 }
805
806 /// BBR passive path model: bottleneck bandwidth in bits/sec, round-trip
807 /// propagation delay in microseconds, and the bandwidth-delay product in
808 /// blocks - all recovered from the ACK stream with no probe traffic. The
809 /// BDP is the in-flight window that keeps the bottleneck busy without a
810 /// standing queue. Diagnostics / window-sizing input.
811 pub fn btlbw_bps(&self) -> u64 {
812 self.path_model.btlbw_bps()
813 }
814
815 pub fn rtprop_us(&self) -> u64 {
816 self.path_model.rtprop_us()
817 }
818
819 pub fn bdp_blocks(&self) -> u64 {
820 self.path_model.bdp_blocks()
821 }
822
823 /// Estimated Wi-Fi backhaul-hop count (0..=3) behind the first hop, from the
824 /// first-hop PHY rate (item 5) vs the measured `BtlBw` (item 6), gated on a
825 /// healthy first hop and inflated RTT. Nonzero answers "are we behind a
826 /// Wi-Fi-backhauled repeater" - which TTL cannot, since an L2 bridge does
827 /// not decrement it. Diagnostics / parity-bias input.
828 pub fn backhaul_hops(&self) -> u8 {
829 self.path_model.backhaul_hops(
830 self.link_phy_kbps as u64 * 1000,
831 self.link_mcs_norm,
832 self.congestion_fraction,
833 )
834 }
835
836 /// The first-hop Wi-Fi PHY rate in Mbit/s (`nominal`) the link sensor read,
837 /// or 0 off Wi-Fi. The mesh-hop count is `round(log2(this / BtlBw))` gated.
838 /// Diagnostics.
839 pub fn first_hop_mbps(&self) -> f32 {
840 self.link_phy_kbps as f32 / 1000.0
841 }
842
843 /// The link class, with the RTT-shape fingerprint filling in for the OS read
844 /// when it is unavailable: if the local sensor returned `Unknown` but the
845 /// end-to-end RTT distribution is clearly bimodal, a Wi-Fi hop is on the
846 /// path, so the class is reported as Wi-Fi.
847 fn inferred_link_class(&self) -> LinkClass {
848 if self.link_class == LinkClass::Unknown
849 && self.rtt_shape.wifi_confidence() > WIFI_SHAPE_CONFIDENCE
850 {
851 LinkClass::Wifi
852 } else {
853 self.link_class
854 }
855 }
856
857 /// Sarle's bimodality coefficient of the RTT distribution (`> 5/9` is
858 /// bimodal - a Wi-Fi hop), or -1 before enough samples. Diagnostics.
859 pub fn rtt_bimodality(&self) -> f32 {
860 self.rtt_shape.bimodality().map(|b| b as f32).unwrap_or(-1.0)
861 }
862
863 /// Confidence in `0..=1` that the path carries a Wi-Fi hop, from the RTT
864 /// shape alone. Diagnostics.
865 pub fn rtt_wifi_confidence(&self) -> f32 {
866 self.rtt_shape.wifi_confidence()
867 }
868
869 /// Self-induced queue delay in milliseconds (`RTT_now - RTprop`): the
870 /// bufferbloat the sender is causing. The LEDBAT pacer holds this near its
871 /// target by sizing the flow window. Diagnostics.
872 pub fn queue_delay_ms(&self) -> f32 {
873 self.path_model.queue_delay_us() as f32 / 1000.0
874 }
875
876 /// Mean RTT in milliseconds across the transfer - the sustained latency the
877 /// bufferbloat pacer holds down. Diagnostics.
878 pub fn rtt_mean_ms(&self) -> f32 {
879 self.path_model.rtt_mean_us() as f32 / 1000.0
880 }
881
882 /// Current in-flight flow window (blocks). Equals the configured maximum on
883 /// an unbloated path; smaller when the bufferbloat pacer has clamped it
884 /// toward the BDP. Diagnostics.
885 pub fn flow_window(&self) -> u32 {
886 self.enc.flow_window()
887 }
888
889 /// Enable or disable the bufferbloat pacer. Disabling restores the full
890 /// flow window and holds it there - the un-paced baseline for an A/B.
891 pub fn set_pacing(&mut self, enabled: bool) {
892 self.pacing_enabled = enabled;
893 if !enabled {
894 self.enc.set_flow_window(self.flow_window_max);
895 }
896 }
897
898 /// Enable or disable proactive burst-recovery on link recovery. Disabling
899 /// falls back to reactive NAK recovery - the A/B baseline.
900 pub fn set_proactive_recovery(&mut self, enabled: bool) {
901 self.proactive_recovery = enabled;
902 }
903
904 /// `true` while the link is declared dead (a PTO of total feedback
905 /// silence). Diagnostics.
906 pub fn link_dead(&self) -> bool {
907 self.link_dead
908 }
909
910 /// Link-liveness telemetry: dead spells detected, probes sent while dead,
911 /// and blocks proactively retransmitted on recovery. Diagnostics.
912 pub fn liveness_stats(&self) -> (u64, u64, u64) {
913 (self.dead_episodes, self.probes_sent, self.recovered_blocks)
914 }
915
916 /// The last recovery interval in milliseconds: time from the link coming
917 /// back to the pre-outage backlog being fully re-delivered. Isolates the
918 /// recovery speed (proactive resend vs reactive NAK learning) from the
919 /// total transfer time. 0 if no recovery has completed. Diagnostics.
920 pub fn recovery_interval_ms(&self) -> f32 {
921 self.recovery_interval_us as f32 / 1000.0
922 }
923
924 /// The shared control table driving this sender.
925 pub fn control(&self) -> &Arc<ControlTable> {
926 &self.control
927 }
928
929 /// `(passthrough_blocks, fec_blocks)` sealed so far. A nonzero first value
930 /// proves the controller dropped FEC fully off the wire (Passthrough) on a
931 /// clean link; the second counts blocks that carried parity.
932 pub fn coding_counts(&self) -> (u64, u64) {
933 self.enc.coding_counts()
934 }
935
936 /// Replace the platform link sensor (e.g. a caller-driven or stub sensor).
937 /// The sensor is a feed-forward loss predictor fused with the receiver's
938 /// measured loss; swapping it lets a caller drive link stress directly.
939 pub fn with_sensor(mut self, sensor: Box<dyn LinkSensor + Send>) -> Self {
940 self.link_sensor = sensor;
941 self
942 }
943
944 /// Replace the fusion policy that maps fused sensor readings to a coding
945 /// decision (level, parity, interleave). The default is
946 /// `ImmediateUpConservativeDown`; a caller can tune the confidence windows
947 /// (how long to drop to Passthrough, how fast to re-arm).
948 pub fn with_fusion(mut self, policy: Box<dyn FusionPolicy + Send>) -> Self {
949 self.fusion = policy;
950 self
951 }
952
953 /// Enable the tower outer code: every `d` data blocks ship with
954 /// `r_outer` fire-and-forget outer-parity blocks that reconstruct
955 /// whole-lost data blocks with no retransmit.
956 pub fn enable_tower(&mut self, d: usize, r_outer: usize) {
957 self.enc.enable_tower(d, r_outer);
958 }
959
960 /// Swap the datagram socket for one the caller already built (a demux socket
961 /// the unified endpoint shares across both codes).
962 pub fn set_sock(&mut self, sock: crate::dgram::DgramSock) {
963 self.sock = sock;
964 }
965
966 /// The bound local address (useful when binding to port 0).
967 pub fn local_addr(&self) -> io::Result<SocketAddr> {
968 self.sock.local_addr()
969 }
970
971 /// Stage and transmit one item. A full block's datagrams pass
972 /// through the interleaver (which holds up to `depth` blocks and
973 /// emits column-major), then any pending feedback is drained so ARQ
974 /// and flow control keep up.
975 pub fn send_item(&mut self, item: &[u8]) -> io::Result<()> {
976 self.sync_interleave()?;
977 let block = self.enc.push(item);
978 if !block.is_empty() {
979 let pkts = self.interleaver.add_block(block);
980 self.send_batch(&pkts)?;
981 // Stamp this block's send time so the ACK that delivers it yields
982 // an RTT for the BBR path model. The block just sealed by `push`
983 // is `next_block_id - 1`. At the default interleave depth (1 =
984 // pass-through) seal time is wire time; deeper interleaving adds a
985 // bounded offset the RTprop min-filter sees through.
986 let sealed = self.enc.next_block_id().wrapping_sub(1);
987 self.block_send_us
988 .push_back((sealed, self.start.elapsed().as_micros() as u64));
989 // Control + feedback ride the per-BLOCK boundary, not every
990 // staged item, so the hot path does not pay a recv syscall
991 // per item (a `k`-fold reduction). Sample the link BEFORE the
992 // heartbeat so the Link frame it carries reports the current
993 // class / quality, not the previous block's.
994 self.maybe_sample_link();
995 self.maybe_send_heartbeat()?;
996 self.maybe_send_bw_probe()?;
997 self.maybe_send_trace()?;
998 self.drain_feedback()?;
999 }
1000 Ok(())
1001 }
1002
1003 /// Flush a short final block and any blocks still buffered in the
1004 /// interleaver.
1005 pub fn flush(&mut self) -> io::Result<()> {
1006 let block = self.enc.flush();
1007 if !block.is_empty() {
1008 let pkts = self.interleaver.add_block(block);
1009 self.send_batch(&pkts)?;
1010 }
1011 let tail = self.interleaver.flush();
1012 self.send_batch(&tail)?;
1013 Ok(())
1014 }
1015
1016 /// Re-read the interleave depth from the control table; on a change,
1017 /// the interleaver flushes its buffered blocks (sent here) before
1018 /// adopting the new depth.
1019 fn sync_interleave(&mut self) -> io::Result<()> {
1020 let want = self.control.interleave_depth() as usize;
1021 if want != self.interleaver.depth() {
1022 let pkts = self.interleaver.set_depth(want);
1023 self.send_batch(&pkts)?;
1024 }
1025 Ok(())
1026 }
1027
1028 /// `true` when in-flight blocks have hit the flow window and the
1029 /// producer should pause until acks free space.
1030 pub fn flow_blocked(&self) -> bool {
1031 self.enc.flow_blocked()
1032 }
1033
1034 /// Unacked blocks held for possible retransmission.
1035 pub fn pending_len(&self) -> usize {
1036 self.enc.pending_len()
1037 }
1038
1039 /// Drain immediately-available feedback (apply acks, send any ARQ)
1040 /// and emit a heartbeat / link sample if due, WITHOUT blocking. Call
1041 /// this in a producer's backpressure loop while
1042 /// [`flow_blocked`](Self::flow_blocked) is true - unlike
1043 /// [`drain_until_acked`](Self::drain_until_acked) it returns at once,
1044 /// so the producer resumes the instant an ack frees window space.
1045 pub fn pump_feedback(&mut self) -> io::Result<()> {
1046 self.maybe_sample_link();
1047 self.maybe_send_heartbeat()?;
1048 self.drain_feedback()
1049 }
1050
1051 /// The forward-loss fraction (0..=1) the receiver last fed back over the
1052 /// control plane. The unified endpoint reads this while RS is the active
1053 /// code to drive the RS -> RLC switch.
1054 pub fn fb_loss(&self) -> f64 {
1055 self.last_fwd_loss as f64
1056 }
1057
1058 /// Drive feedback / ARQ until every block is acked or `timeout`
1059 /// elapses. Call after [`flush`](Self::flush) to guarantee the tail
1060 /// is delivered. Returns `true` if fully acked.
1061 pub fn drain_until_acked(&mut self, timeout: Duration) -> io::Result<bool> {
1062 let start = Instant::now();
1063 while self.enc.pending_len() > 0 {
1064 if start.elapsed() > timeout {
1065 return Ok(false);
1066 }
1067 self.maybe_send_heartbeat()?;
1068 self.drain_feedback()?;
1069 // Brief park so this tail drain is not a busy spin; the
1070 // receiver emits feedback on its own ~20ms timeout cadence.
1071 std::thread::sleep(Duration::from_micros(200));
1072 }
1073 Ok(true)
1074 }
1075
1076 /// Read and apply all immediately-available feedback datagrams,
1077 /// transmitting any ARQ retransmits they request.
1078 fn drain_feedback(&mut self) -> io::Result<()> {
1079 let mut buf = [0u8; CONTROL_RECV_BUF];
1080 loop {
1081 // Standalone path reads the connected socket with the IP-TTL cmsg
1082 // (item 14 reverse-hop count); the demux path has no fd, so it pops
1083 // its queue via the connected recv (no TTL observation there).
1084 let res = match self.sock.as_udp() {
1085 Some(u) => recv_with_ttl(u, &mut buf),
1086 None => self.sock.recv(&mut buf).map(|n| (n, None)),
1087 };
1088 match res {
1089 Ok((n, ttl)) => {
1090 // Item 14 reverse-hop count: the feedback's IP TTL gives how
1091 // many hops the peer's packets crossed on the way back.
1092 if let Some(t) = ttl {
1093 self.asym
1094 .observe_reverse(crate::path_sensor::hop_count_from_ttl(t));
1095 }
1096 if let Some(cp) = decode_control(&buf[..n]) {
1097 // Feedback for another session says nothing about
1098 // this one's blocks, and its ack frontier would
1099 // prune every block still held. Announcing nothing
1100 // is not a mismatch; only a different epoch is.
1101 if cp.session_announce.is_some_and(|e| e != self.enc.epoch()) {
1102 continue;
1103 }
1104 }
1105 if let Some(cp) = decode_control(&buf[..n]) {
1106 // A feedback control packet from the receiver: count it,
1107 // and read its LossAcct to learn how many feedback
1108 // packets the receiver sent (peer_seq). The reverse-path
1109 // loss is what we missed, as a share of what it sent -
1110 // distinct from the forward data loss the receiver
1111 // measures. The in-flight is negligible at scale, so the
1112 // ratio converges to the loss fraction.
1113 self.ctrl_recv = self.ctrl_recv.wrapping_add(1);
1114 // Echo the challenge verbatim. Answering it is the
1115 // proof, and only a peer receiving at the claimed
1116 // address can answer.
1117 if let Some(sc) = cp.session_challenge {
1118 let mut ans = ControlPacket::new();
1119 ans.session_response = Some(sc);
1120 let wire = encode_control(&ans);
1121 self.sock.send(&wire).ok();
1122 }
1123 // Link-liveness: ANY feedback means the link is alive.
1124 // Note whether we were dead; the proactive recovery
1125 // burst fires AFTER `on_feedback` below applies this
1126 // ACK, so it resends only the still-unacked (genuinely
1127 // lost) blocks - not the whole window, most of which a
1128 // dead-link recovery ACK frees at once (the data
1129 // arrived; only the ACKs were lost).
1130 self.last_feedback_at = Instant::now();
1131 let was_dead = self.link_dead;
1132 self.link_dead = false;
1133 // Begin a recovery-interval measurement (both modes):
1134 // the frontier must climb to the highest block sent so
1135 // far for the pre-outage backlog to be fully delivered.
1136 if was_dead {
1137 self.recovery_target = self.enc.next_block_id();
1138 self.recovery_started_us = self.start.elapsed().as_micros() as u64;
1139 }
1140 if let Some(la) = cp.loss_acct {
1141 if la.seq > self.peer_seq {
1142 self.peer_seq = la.seq;
1143 }
1144 let missed = self.peer_seq.saturating_sub(self.ctrl_recv);
1145 self.rev_loss =
1146 (missed as f32 / self.peer_seq.max(1) as f32).clamp(0.0, 1.0);
1147 }
1148 // Feed the path sensor before fusion, so a hop-count
1149 // shift or ECN congestion in this packet is already
1150 // reflected when the controller recomputes.
1151 if let Some(p) = cp.path {
1152 self.path_sensor.observe(p.ttl, p.ecn, p.hop_count);
1153 // Item 14 forward-hop count: how many hops the peer
1154 // reports OUR packets crossed (vs the reverse above).
1155 self.asym.observe_forward(p.hop_count);
1156 // Item 15 AccECN: the graded CE rate is the peer's
1157 // cumulative CE marks over its ECN-capable packets.
1158 // The cumulative ratio (not a per-feedback delta) is
1159 // what stays stable: feedback fires every few packets,
1160 // so at a low mark rate most intervals see zero new CE
1161 // marks and a per-frame delta reads a noisy 0 - the
1162 // running ratio is the AQM's mark rate directly.
1163 if p.ect_count > 0 {
1164 self.ce_rate =
1165 (p.ce_count as f32 / p.ect_count as f32).clamp(0.0, 1.0);
1166 }
1167 }
1168 // The peer's egress MTU: a drop is a peer-side path
1169 // event (a lower-MTU link engaged at the other end), so
1170 // spike the shift to pre-arm this end too.
1171 if let Some(pm) = cp.pmtu {
1172 if self.peer_pmtu != 0 && pm.pmtu != 0 && pm.pmtu < self.peer_pmtu {
1173 self.peer_pmtu_shift = 1.0;
1174 }
1175 if pm.pmtu != 0 {
1176 self.peer_pmtu = pm.pmtu;
1177 }
1178 }
1179 // WBest report (item 13): the receiver's available-
1180 // bandwidth / effective-capacity estimate, held for
1181 // telemetry and the cross-check against the passive BtlBw.
1182 if let Some(ab) = cp.avail_bw {
1183 self.avail_bw_kbps = ab.avail_kbps;
1184 self.wbest_capacity_kbps = ab.capacity_kbps;
1185 }
1186 // Sprout forecast (item 16): the receiver's next-tick
1187 // deliverable-rate lower bound, in bytes/s, used to
1188 // pre-size the flow window ahead of a dip.
1189 if let Some(fc) = cp.forecast {
1190 self.forecast_bps = fc.forecast_kbps * 1000 / 8;
1191 }
1192 // LEO cadence (item 17): the receiver's detected handover
1193 // period and time-to-next-spike, for the pre-arm.
1194 if let Some(pe) = cp.periodicity {
1195 self.leo_period_s = pe.period_ds as f32 / 10.0;
1196 self.leo_secs_to_spike = pe.secs_to_spike_ds as f32 / 10.0;
1197 self.leo_conf = pe.confidence_x255 as f32 / 255.0;
1198 }
1199 let fb = feedback_from_control(&cp);
1200 let rtx = self.enc.on_feedback(&fb);
1201 self.send_batch(&rtx)?;
1202 // Proactive recovery: now that this ACK has freed every
1203 // block the receiver actually got, ENQUEUE whatever is
1204 // STILL unacked oldest-first - the genuinely-lost gap -
1205 // for a BtlBw-paced resend, instead of waiting a round
1206 // trip per NAK to relearn it. The resend is metered
1207 // (`drain_recovery`) so it fills the pipe without
1208 // overflowing, and the pacer is told to expect it.
1209 if was_dead && self.proactive_recovery {
1210 let gap = self.enc.retransmit_all_data();
1211 if !gap.is_empty() {
1212 self.recovered_blocks += self.enc.pending_len() as u64;
1213 self.recovery_dgrams.extend(gap);
1214 self.last_recovery_us = self.start.elapsed().as_micros() as u64;
1215 self.recovery_tokens = 0.0;
1216 }
1217 }
1218 // Recovery complete once the frontier reaches the target
1219 // captured at the dead->alive transition (the whole
1220 // pre-outage backlog re-delivered). Record the interval.
1221 if self.recovery_target != 0 && fb.ack_through >= self.recovery_target {
1222 self.recovery_interval_us = (self.start.elapsed().as_micros() as u64)
1223 .saturating_sub(self.recovery_started_us);
1224 self.recovery_target = 0;
1225 }
1226 // BBR passive path model: pop the send times of every
1227 // block this ACK delivered; the freshest (highest id)
1228 // gives the round-trip time, and the cumulative
1229 // `ack_through` gives the delivered count. The model's
1230 // own anchored sampling window guards against coalesced
1231 // ACKs, so no send-span is needed here.
1232 let now_us = self.start.elapsed().as_micros() as u64;
1233 let mut rtt_us = 0u64;
1234 let mut newest_send = 0u64;
1235 while let Some(&(id, sent)) = self.block_send_us.front() {
1236 if id < fb.ack_through {
1237 newest_send = sent;
1238 rtt_us = now_us.saturating_sub(sent);
1239 self.block_send_us.pop_front();
1240 } else {
1241 break;
1242 }
1243 }
1244 self.path_model
1245 .on_ack(fb.ack_through as u64, now_us, rtt_us, newest_send);
1246 // Fold the RTT into the shape fingerprint: a bimodal
1247 // distribution is the signature of a Wi-Fi hop.
1248 if rtt_us > 0 {
1249 self.rtt_shape.observe(rtt_us as f64);
1250 }
1251 self.apply_fusion(&fb);
1252 }
1253 }
1254 Err(e)
1255 if e.kind() == io::ErrorKind::WouldBlock
1256 || e.kind() == io::ErrorKind::TimedOut
1257 || e.kind() == io::ErrorKind::ConnectionReset
1258 || e.kind() == io::ErrorKind::ConnectionRefused
1259 || e.kind() == io::ErrorKind::HostUnreachable
1260 || e.kind() == io::ErrorKind::NetworkUnreachable =>
1261 {
1262 // A pending ICMP error the kernel surfaces on a regular recv
1263 // because IP_RECVERR is on: a port-unreachable (peer not up -
1264 // ConnectionReset on Windows, ConnectionRefused on Linux/BSD)
1265 // or a TTL-expired-in-transit from our own item-14 Trace
1266 // probes (HostUnreachable). None is a real connection
1267 // failure; the error queue is drained separately for the
1268 // trace, so ignore it here rather than kill the transfer.
1269 break;
1270 }
1271 Err(e) => return Err(e),
1272 }
1273 }
1274 self.drain_recovery()?;
1275 self.check_liveness()?;
1276 Ok(())
1277 }
1278
1279 /// Meter the proactive-recovery resend at the item-6 BtlBw rate (a token
1280 /// bucket): send as many queued gap datagrams as the accrued byte budget
1281 /// allows, so the whole gap refills the pipe at the bottleneck rate -
1282 /// far faster than reactive one-block-per-round-trip NAK recovery, yet
1283 /// without the buffer overflow an unpaced dump caused. While draining (and
1284 /// for a few round trips after) it arms the pacer grace, so the queue this
1285 /// adds is not mistaken for steady-state bloat.
1286 fn drain_recovery(&mut self) -> io::Result<()> {
1287 if self.recovery_dgrams.is_empty() {
1288 return Ok(());
1289 }
1290 let now_us = self.start.elapsed().as_micros() as u64;
1291 let elapsed = now_us.saturating_sub(self.last_recovery_us);
1292 self.last_recovery_us = now_us;
1293 let rate_bytes = (self.path_model.btlbw_bps() / 8).max(MIN_RECOVERY_BYTES_PER_S) as f64;
1294 self.recovery_tokens += rate_bytes * elapsed as f64 / 1_000_000.0;
1295 if self.recovery_tokens > RECOVERY_BUCKET_BYTES {
1296 self.recovery_tokens = RECOVERY_BUCKET_BYTES;
1297 }
1298 while let Some(front) = self.recovery_dgrams.front() {
1299 let size = front.len() as f64;
1300 if self.recovery_tokens < size {
1301 break;
1302 }
1303 self.recovery_tokens -= size;
1304 let dgram = self.recovery_dgrams.pop_front().expect("front exists");
1305 self.send(&dgram)?;
1306 }
1307 // Hold the pacer through the resend and a few round trips after, so the
1308 // recovery's transient queue clears before normal control resumes.
1309 let grace = RECOVERY_GRACE_RTTS * self.path_model.rtt_now_us().max(MIN_PACE_INTERVAL_US);
1310 self.recovery_grace_until_us = now_us + grace;
1311 Ok(())
1312 }
1313
1314 /// Declare the link dead after a PTO of total feedback silence, and while
1315 /// dead send a periodic probe - a retransmit of the oldest unacked block -
1316 /// which both elicits feedback (so recovery is noticed regardless of the
1317 /// receiver's own cadence) and pre-positions the block the receiver's
1318 /// frontier is stalled on. New data is already held by flow-control
1319 /// backpressure (the window cannot advance with no ACKs), so this is the
1320 /// only traffic the dead state adds beyond the cheap heartbeat.
1321 fn check_liveness(&mut self) -> io::Result<()> {
1322 let silence_us = self.last_feedback_at.elapsed().as_micros() as u64;
1323 let dead_timeout =
1324 (DEAD_RTT_MULTIPLE * self.path_model.rtt_now_us()).max(DEAD_FLOOR_US);
1325 if silence_us <= dead_timeout {
1326 return Ok(());
1327 }
1328 if !self.link_dead {
1329 self.link_dead = true;
1330 self.dead_episodes += 1;
1331 }
1332 // Probe at the dead-timeout cadence while the link stays dark.
1333 if self.last_probe_at.elapsed().as_micros() as u64 >= dead_timeout
1334 && let Some(oldest) = self.enc.oldest_pending()
1335 {
1336 let probe = self.enc.probe_block(oldest);
1337 if !probe.is_empty() {
1338 self.send_batch(&probe)?;
1339 self.probes_sent += 1;
1340 }
1341 self.last_probe_at = Instant::now();
1342 }
1343 Ok(())
1344 }
1345
1346 /// Emit a heartbeat (timestamp + ring-shape digest) if the interval
1347 /// has elapsed. The timestamp lets the receiver measure the OWD
1348 /// trend; the digest lets it forecast demand.
1349 fn maybe_send_heartbeat(&mut self) -> io::Result<()> {
1350 if self.last_hb.elapsed() >= HEARTBEAT_INTERVAL {
1351 let mut cp = ControlPacket::new();
1352 // Which session this endpoint is sending under. The beat
1353 // reaches a receiver still bound to a dead predecessor, which
1354 // the data does not.
1355 cp.session_announce = Some(self.enc.epoch());
1356 // The clock beat: drives the receiver's OWD-trend slope and jitter.
1357 cp.timing = Some(TimingFrame {
1358 send_ts: self.start.elapsed().as_micros() as u64,
1359 echo_ts: 0,
1360 });
1361 // Source-ring shape (the legacy heartbeat payload, now a frame).
1362 // Backlog proxy: in-flight blocks (the real AdaptiveIpc integration
1363 // reads the source ring's fill instead).
1364 cp.ring = Some(RingFrame {
1365 fill_pct: self.enc.in_flight().min(255) as u8,
1366 ring_kind: 0,
1367 producers: 1,
1368 consumers: 1,
1369 trend: 1,
1370 flags: 0,
1371 });
1372 // Bidirectional loss accounting: our heartbeat-send count and how
1373 // many feedback packets we have received, so the receiver can tell
1374 // its feedback is reaching us (and shorten its cadence if not).
1375 self.ctrl_out = self.ctrl_out.wrapping_add(1);
1376 cp.loss_acct = Some(LossAcctFrame {
1377 seq: self.ctrl_out,
1378 last_recv_seq: self.ctrl_recv,
1379 });
1380 // Our link class + quality, so the peer knows what kind of link
1381 // (Wi-Fi / wired / cellular) carries this end of the path. The class
1382 // falls back to the RTT-shape fingerprint when the OS read is
1383 // unavailable.
1384 cp.link = Some(LinkFrame {
1385 class: self.inferred_link_class().as_u8(),
1386 quality: self.link_quality,
1387 });
1388 // Our egress path MTU, so the peer can track a handoff on this end.
1389 if let Some(pm) = self.net_events.pmtu() {
1390 cp.pmtu = Some(PmtuFrame { pmtu: pm });
1391 }
1392 let buf = encode_control(&cp);
1393 self.send(&buf)?;
1394 self.last_hb = Instant::now();
1395 }
1396 Ok(())
1397 }
1398
1399 /// Emit one WBest probe round (item 13): `BW_PROBE_PAIRS` back-to-back packet
1400 /// pairs (stage 1, effective capacity) followed by a `BW_PROBE_TRAIN`-packet
1401 /// train (stage 2, available bandwidth). Every probe is a control datagram
1402 /// padded to `BW_PROBE_BYTES` carrying a single `BwProbe` frame stamped with
1403 /// the round id and its index; the receiver measures the dispersions and
1404 /// reports the estimate back. Sent as one burst so the bottleneck serializes
1405 /// the packets, which is what the dispersion measures.
1406 fn maybe_send_bw_probe(&mut self) -> io::Result<()> {
1407 if self.last_bw_probe.elapsed() < BW_PROBE_INTERVAL {
1408 return Ok(());
1409 }
1410 let round = self.bw_probe_round;
1411 self.bw_probe_round = self.bw_probe_round.wrapping_add(1);
1412 let total = 2 * BW_PROBE_PAIRS + BW_PROBE_TRAIN;
1413 for idx in 0..total {
1414 let mut cp = ControlPacket::new();
1415 cp.bw_probe.push(crate::control_frame::BwProbeFrame {
1416 probe_id: round,
1417 idx,
1418 send_ts: self.start.elapsed().as_micros() as u64,
1419 });
1420 let mut buf = encode_control(&cp);
1421 crate::control_frame::pad_control_to(&mut buf, BW_PROBE_BYTES);
1422 self.send(&buf)?;
1423 }
1424 self.last_bw_probe = Instant::now();
1425 Ok(())
1426 }
1427
1428 /// The receiver's most recent WBest report: (available bandwidth, effective
1429 /// capacity) in bits/s, both 0 until the first report lands. The sender
1430 /// cross-checks the capacity against its passive [`btlbw_bps`](Self::btlbw_bps).
1431 pub fn avail_bw_bps(&self) -> (u64, u64) {
1432 (self.avail_bw_kbps * 1000, self.wbest_capacity_kbps * 1000)
1433 }
1434
1435 /// Emit one Trace sweep (item 14): a probe at each IP TTL 1..=`MAX_TRACE_HOPS`,
1436 /// stamping the per-TTL send time, then drain whatever ICMP TimeExceeded
1437 /// replies have arrived. Linux only (the error queue is an `IP_RECVERR`
1438 /// capability); a no-op elsewhere.
1439 #[cfg(target_os = "linux")]
1440 fn maybe_send_trace(&mut self) -> io::Result<()> {
1441 use std::os::fd::AsRawFd;
1442 // Trace (the IP_RECVERR error queue) needs the kernel fd; the demux path
1443 // has none, so trace is simply off there (a sensor, not correctness).
1444 let Some(fd) = self.sock.as_udp().map(|u| u.as_raw_fd()) else {
1445 return Ok(());
1446 };
1447 if self.last_trace.elapsed() >= TRACE_INTERVAL {
1448 self.trace_round = self.trace_round.wrapping_add(1);
1449 let now = self.start.elapsed().as_micros() as u64;
1450 for ttl in 1..=MAX_TRACE_HOPS {
1451 let mut cp = ControlPacket::new();
1452 cp.trace.push(crate::control_frame::TraceFrame {
1453 hop_ttl: ttl,
1454 probe_id: self.trace_round,
1455 });
1456 let buf = encode_control(&cp);
1457 // A probe send may surface a prior probe's latched ICMP error
1458 // (IP_RECVERR); that is the trace working, not a failure, so a
1459 // send error here just means this probe is skipped this round.
1460 crate::trace_sensor::send_at_ttl(fd, self.trace_peer, &buf, ttl)
1461 .ok();
1462 self.trace_send_us[ttl as usize] = now;
1463 }
1464 self.last_trace = Instant::now();
1465 }
1466 let now = self.start.elapsed().as_micros() as u64;
1467 for (router, payload) in crate::trace_sensor::drain_icmp_errors(fd) {
1468 // The expired probe's payload is echoed back; its Trace frame's TTL
1469 // is the hop index, and `now - send_time[ttl]` is the per-hop RTT.
1470 if let Some(cp) = decode_control(&payload)
1471 && let Some(tf) = cp.trace.first()
1472 {
1473 let ttl = tf.hop_ttl;
1474 let sent = self.trace_send_us.get(ttl as usize).copied().unwrap_or(0);
1475 let rtt_us = now.saturating_sub(sent);
1476 if !self.trace_hops.iter().any(|h| h.ttl == ttl) {
1477 self.trace_hops.push(crate::trace_sensor::TraceHop {
1478 ttl,
1479 addr: router,
1480 rtt_us,
1481 });
1482 self.trace_hops.sort_by_key(|h| h.ttl);
1483 }
1484 }
1485 }
1486 Ok(())
1487 }
1488
1489 #[cfg(not(target_os = "linux"))]
1490 fn maybe_send_trace(&mut self) -> io::Result<()> {
1491 Ok(())
1492 }
1493
1494 /// The hops the Trace sweep discovered toward the peer (item 14): each is a
1495 /// `(ttl, router address, RTT)` from an ICMP TimeExceeded.
1496 pub fn trace_hops(&self) -> &[crate::trace_sensor::TraceHop] {
1497 &self.trace_hops
1498 }
1499
1500 /// Forward / reverse path hop counts and their asymmetry (item 14), or `None`
1501 /// for a direction not yet observed.
1502 pub fn path_asymmetry(&self) -> (Option<u8>, Option<u8>, Option<u8>) {
1503 (self.asym.forward(), self.asym.reverse(), self.asym.asymmetry())
1504 }
1505
1506 /// The graded AccECN CE rate (item 15): the fraction of our ECN-capable
1507 /// packets the AQM marked CE, `delta_CE / delta_ECT` from the peer's counts.
1508 pub fn ce_rate(&self) -> f32 {
1509 self.ce_rate
1510 }
1511
1512 /// The peer's Sprout forecast (item 16): the 5th-percentile next-tick
1513 /// deliverable rate (bits/s), 0 until the first forecast arrives. Drives the
1514 /// predictive window cap and leads a dip down.
1515 pub fn forecast_bps(&self) -> u64 {
1516 self.forecast_bps * 8
1517 }
1518
1519 /// The LEO pre-arm path-shift (item 17): the detection confidence when a
1520 /// confident handover cadence's next spike is within the pre-arm window,
1521 /// else 0 - so protection arms one cycle ahead of the spike.
1522 fn leo_prearm_shift(&self) -> f32 {
1523 const LEO_PRE_ARM_WINDOW_S: f32 = 2.0;
1524 if self.leo_conf >= 0.4
1525 && self.leo_period_s > 0.0
1526 && self.leo_secs_to_spike <= LEO_PRE_ARM_WINDOW_S
1527 {
1528 self.leo_conf
1529 } else {
1530 0.0
1531 }
1532 }
1533
1534 /// The peer's detected LEO handover cadence (item 17): `(period_s,
1535 /// confidence, secs_to_next_spike)`. `period_s == 0` means none detected.
1536 pub fn leo_cadence(&self) -> (f32, f32, f32) {
1537 (self.leo_period_s, self.leo_conf, self.leo_secs_to_spike)
1538 }
1539
1540 /// Poll the platform link sensor on the slow cadence and cache its
1541 /// stress reading for the fusion controller.
1542 fn maybe_sample_link(&mut self) {
1543 if self.last_link_sample.elapsed() >= LINK_SAMPLE_INTERVAL {
1544 let snap = self.link_sensor.sample();
1545 self.link_stress = snap.link_stress();
1546 // A class change (a handoff) is a path event: spike the shift so the
1547 // controller pre-arms, the same way a hop-count change does. Skip the
1548 // first reading (Unknown -> something is not a handoff).
1549 if self.link_class != LinkClass::Unknown && self.link_class != snap.class {
1550 self.class_shift = 1.0;
1551 }
1552 self.link_class = snap.class;
1553 self.link_quality = snap
1554 .signal_quality
1555 .unwrap_or(((1.0 - self.link_stress) * 100.0) as u8);
1556 // First-hop PHY rate + MCS for mesh-hop detection.
1557 self.link_phy_kbps = snap.phy_rate_kbps.unwrap_or(0);
1558 self.link_mcs_norm = snap.mcs_norm.unwrap_or(0.0);
1559 self.last_link_sample = Instant::now();
1560 }
1561 // Decay the class-shift and the peer-MTU-drop shift each poll so the
1562 // handoff pre-arms fade (the net-event shift decays on its own clock).
1563 self.class_shift *= 0.9;
1564 self.peer_pmtu_shift *= 0.9;
1565 }
1566
1567 /// Run the fusion controller on the receiver's reported sensors plus
1568 /// the local link sensor, and publish the resulting coding knobs into
1569 /// the control table and the encoder. This is where the adaptive loop
1570 /// closes on the sender.
1571 fn apply_fusion(&mut self, fb: &crate::reliable_udp::Feedback) {
1572 // Fold the peer's loss-class report into the congestion-share EWMA, but
1573 // only on a feedback that carries loss (code 0 = no loss holds the
1574 // share, so it reflects the last loss regime when loss resumes).
1575 // 2 = congestion -> 1.0, 3 = mixed -> 0.5, 1 = wireless -> 0.0.
1576 if fb.loss_class != 0 {
1577 let contribution = match fb.loss_class {
1578 2 => 1.0,
1579 3 => 0.5,
1580 _ => 0.0,
1581 };
1582 self.congestion_fraction += (contribution - self.congestion_fraction) * 0.125;
1583 }
1584 // The event-driven path shift (OS observer or peer-MTU-drop), captured
1585 // once so its transient peak is held for end-of-run telemetry even as
1586 // the live value decays.
1587 let event_shift = self.net_events.path_shift().max(self.peer_pmtu_shift);
1588 self.net_event_shift_peak = self.net_event_shift_peak.max(event_shift);
1589 let snap = SensorSnapshot {
1590 loss: fb.loss_x255 as f32 / 255.0,
1591 burstiness: fb.burstiness_x255 as f32 / 255.0,
1592 owd_trend: match fb.owd_trend_class {
1593 2 => 0.1,
1594 0 => -0.1,
1595 _ => 0.0,
1596 },
1597 link_stress: self.link_stress,
1598 // A path shift from any of four feed-forward sources: the passive
1599 // hop-count change (`path_sensor`), a link-class handoff
1600 // (`class_shift`), an OS-announced route / carrier / MTU event
1601 // (`net_events`, ahead of any loss), or a peer-side MTU drop
1602 // (`peer_pmtu_shift`). The strongest wins.
1603 path_shift: self
1604 .path_sensor
1605 .path_shift()
1606 .max(self.class_shift)
1607 .max(event_shift)
1608 // LEO pre-arm (item 17): when the peer has detected a confident
1609 // handover cadence and its next spike is within the pre-arm
1610 // window, spike the path shift NOW - one cycle ahead of the delay
1611 // spike, so protection is armed before the handover lands.
1612 .max(self.leo_prearm_shift()),
1613 // AccECN graded CE rate (item 15) when the peer reports counters;
1614 // the path-sensor's single-CE-bit reading is the floor so a first CE
1615 // still registers before the rate has accumulated.
1616 ecn_ce: self.ce_rate.max(self.path_sensor.ecn_ce()),
1617 congestion_fraction: self.congestion_fraction,
1618 rev_loss: self.rev_loss,
1619 // Self-induced queue delay from the BBR path model (item 6 RTprop):
1620 // RTT_now - RTprop, the bufferbloat signal.
1621 queue_delay_ms: self.path_model.queue_delay_us() as f32 / 1000.0,
1622 // Wi-Fi backhaul-hop estimate (item 5 first-hop PHY vs item 6 BtlBw):
1623 // more hops bias parity up.
1624 backhaul_hops: self.backhaul_hops(),
1625 };
1626 self.last_fwd_loss = snap.loss;
1627 let d = self.fusion.decide(&snap);
1628 self.control.set_level(d.level);
1629 self.control.set_parity_r(d.parity_r);
1630 self.control.set_interleave_depth(d.interleave_depth);
1631 // Provision parity to actually COVER the measured loss for this block's k
1632 // (r/(k+r) >= loss), with the controller's decision as the floor - so a
1633 // high-loss block recovers in-FEC up to the bitmap ceiling instead of
1634 // falling to ARQ round trips at the old fixed parity<=6.
1635 self.enc.set_parity_covering(d.parity_r as usize, snap.loss);
1636 self.pace_flow_window(snap.queue_delay_ms);
1637 }
1638
1639 /// LEDBAT delay-based pacer (RFC 6817): hold the self-induced queue near
1640 /// [`PACE_TARGET_MS`] by nudging the flow window once per round trip in
1641 /// proportion to how far the measured queue delay is from target. When the
1642 /// queue is deeper than target the window shrinks (drain); when it is
1643 /// shallower it grows (probe), each step bounded so one round trip never
1644 /// cuts the window by more than half. This settles the window at the size
1645 /// that keeps the bottleneck busy with about one target's worth of queue,
1646 /// rather than the binary snap-to-BDP / snap-to-full that oscillated.
1647 ///
1648 /// On a clean link the queue delay is ~0, so `off_target` stays positive
1649 /// and the window holds at its full configured value - the pacer only ever
1650 /// engages once WE are the ones filling a buffer.
1651 fn pace_flow_window(&mut self, queue_delay_ms: f32) {
1652 if !self.pacing_enabled {
1653 return;
1654 }
1655 // Recovery grace: while a proactive resend is in flight (or for a few
1656 // round trips after), the queue is an expected, intentional transient -
1657 // not steady-state bloat - so hold the window rather than clamp it. This
1658 // is what lets the recovery refill the pipe without the pacer then
1659 // throttling the very window it restored.
1660 let now = self.start.elapsed().as_micros() as u64;
1661 if !self.recovery_dgrams.is_empty() || now < self.recovery_grace_until_us {
1662 return;
1663 }
1664 // The queue responds one CURRENT round trip after a window change (the
1665 // inflated RTT under load, not the bloat-free RTprop), so adjust at most
1666 // once per smoothed RTT - adjusting faster than the feedback loop closes
1667 // over-corrects and oscillates. Fall back to a 1 ms floor before the
1668 // first RTT sample lands.
1669 let interval = self.path_model.rtt_now_us().max(MIN_PACE_INTERVAL_US);
1670 if now < self.last_pace_us + interval {
1671 return;
1672 }
1673 self.last_pace_us = now;
1674 // off_target: +1 when the queue is empty, 0 at target, negative when the
1675 // queue is deeper than target. The step is clamped so a single round
1676 // trip never removes more than half the window.
1677 let off_target = (PACE_TARGET_MS - queue_delay_ms) / PACE_TARGET_MS;
1678 let w = self.paced_window;
1679 let step = off_target.clamp(-0.5 * w, w);
1680 self.paced_window = (w + step).clamp(MIN_PACED_WINDOW as f32, self.flow_window_max as f32);
1681 let mut target = self.paced_window.round() as u32;
1682 // Item 16 predictive cap: when the Sprout forecast (the conservative
1683 // next-tick deliverable rate) falls well below the historical BtlBw, a
1684 // dip is coming - scale the window down NOW, before the queue (and the
1685 // loss) the dip would cause builds. The LEDBAT step above only reacts
1686 // after the queue has formed; this leads it. The `FORECAST_HEADROOM`
1687 // factor leaves room to send ABOVE the forecast, so the sender keeps
1688 // probing the link and the forecast can climb back after a dip - without
1689 // it the cap is self-reinforcing (the send rate collapses to the forecast,
1690 // so the arrivals the forecast is built from never reveal a faster link).
1691 let btlbw = self.path_model.btlbw_bps();
1692 if self.forecast_bps > 0 && btlbw > 0 {
1693 let ratio =
1694 (self.forecast_bps as f64 * FORECAST_HEADROOM / btlbw as f64).clamp(0.1, 1.0);
1695 let cap = ((self.flow_window_max as f64 * ratio).ceil() as u32).max(MIN_PACED_WINDOW);
1696 target = target.min(cap);
1697 }
1698 if target != self.enc.flow_window() {
1699 self.enc.set_flow_window(target);
1700 }
1701 }
1702
1703 fn send(&self, pkt: &[u8]) -> io::Result<()> {
1704 let mut spins = 0u32;
1705 loop {
1706 match self.sock.send(pkt) {
1707 Ok(_) => return Ok(()),
1708 // Send buffer full = the link is saturated. PACE: wait for
1709 // buffer space instead of dropping. Dropping here
1710 // manufactures loss and lets the sender outrun the link,
1711 // so FEC/ARQ then has to recover the sender's OWN datagrams
1712 // - a throughput collapse, not a wire loss.
1713 Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
1714 spins += 1;
1715 if spins > 20_000 {
1716 // ~1s saturated: the peer is likely gone; let
1717 // ARQ / FEC cope rather than spin forever.
1718 return Ok(());
1719 }
1720 std::thread::sleep(Duration::from_micros(50));
1721 }
1722 // A pending ICMP error the connected socket surfaces on send:
1723 // a reset / refused (peer not up), or a TTL-expired-in-transit
1724 // that IP_RECVERR latched from our own item-14 Trace probes
1725 // (HostUnreachable / NetworkUnreachable). None is a real send
1726 // failure; drop this datagram and let ARQ / FEC recover.
1727 Err(e)
1728 if matches!(
1729 e.kind(),
1730 io::ErrorKind::ConnectionReset
1731 | io::ErrorKind::ConnectionRefused
1732 | io::ErrorKind::HostUnreachable
1733 | io::ErrorKind::NetworkUnreachable
1734 ) =>
1735 {
1736 return Ok(());
1737 }
1738 Err(e) => return Err(e),
1739 }
1740 }
1741 }
1742
1743 /// Send a whole block's datagrams. On Linux this uses UDP GSO
1744 /// (`UDP_SEGMENT`): the same-size datagrams concatenate into ONE buffer
1745 /// the kernel segments into many wire datagrams, so a block costs one
1746 /// `sendmsg` and one skb instead of `k+r` skbs - the clean-link
1747 /// throughput lever QUIC uses. The kernel splits on the wire, so the
1748 /// receiver is unchanged. On Windows it is USO (`WSASendMsg` with the
1749 /// `UDP_SEND_MSG_SIZE` control message), the same one-buffer/kernel-
1750 /// segments model. On FreeBSD it is one `sendmmsg` per batch; elsewhere
1751 /// one `send` per datagram. Falls back to per-datagram sends if the
1752 /// kernel lacks segmentation offload. Pacing and ICMP-reset handling
1753 /// match [`send`](Self::send).
1754 fn send_batch(&self, pkts: &[Vec<u8>]) -> io::Result<()> {
1755 if pkts.is_empty() {
1756 return Ok(());
1757 }
1758 #[cfg(target_os = "linux")]
1759 {
1760 self.send_gso(pkts)
1761 }
1762 #[cfg(target_os = "freebsd")]
1763 {
1764 self.send_mmsg(pkts)
1765 }
1766 #[cfg(target_os = "windows")]
1767 {
1768 // `SUBETHA_USO=0` forces the per-datagram path - the A/B baseline
1769 // for measuring the USO segmentation win in one harness.
1770 if uso_enabled() {
1771 self.send_uso(pkts)
1772 } else {
1773 for pkt in pkts {
1774 self.send(pkt)?;
1775 }
1776 Ok(())
1777 }
1778 }
1779 #[cfg(not(any(
1780 target_os = "linux",
1781 target_os = "freebsd",
1782 target_os = "windows"
1783 )))]
1784 {
1785 for pkt in pkts {
1786 self.send(pkt)?;
1787 }
1788 Ok(())
1789 }
1790 }
1791
1792 /// UDP GSO egress (Linux). Groups consecutive same-size datagrams (GSO
1793 /// requires a uniform segment size) into one buffer of up to 64
1794 /// segments / 60 KiB and sends each group with a `UDP_SEGMENT` control
1795 /// message; the kernel segments it into individual wire datagrams. A
1796 /// lone datagram takes the plain paced `send`. If the kernel rejects
1797 /// GSO, the rest of the batch falls back to `sendmmsg`.
1798 #[cfg(target_os = "linux")]
1799 fn send_gso(&self, pkts: &[Vec<u8>]) -> io::Result<()> {
1800 use std::os::fd::AsRawFd;
1801 // The demux path has no kernel fd for GSO; send each datagram plainly.
1802 let fd = match self.sock.as_udp() {
1803 Some(u) => u.as_raw_fd(),
1804 None => {
1805 for p in pkts {
1806 self.sock.send(p)?;
1807 }
1808 return Ok(());
1809 }
1810 };
1811 let mut buf: Vec<u8> = Vec::with_capacity(64 * 1500);
1812 let mut i = 0usize;
1813 while i < pkts.len() {
1814 let seg = pkts[i].len();
1815 buf.clear();
1816 let mut j = i;
1817 while j < pkts.len()
1818 && pkts[j].len() == seg
1819 && (j - i) < 64
1820 && buf.len() + seg <= 61440
1821 {
1822 buf.extend_from_slice(&pkts[j]);
1823 j += 1;
1824 }
1825 if j - i <= 1 || seg == 0 || seg > u16::MAX as usize {
1826 self.send(&pkts[i])?;
1827 i += 1;
1828 continue;
1829 }
1830 if !self.send_gso_buf(fd, &buf, seg as u16)? {
1831 // Kernel lacks GSO: send the remaining datagrams plainly.
1832 return self.send_mmsg(&pkts[i..]);
1833 }
1834 i = j;
1835 }
1836 Ok(())
1837 }
1838
1839 /// One `sendmsg` with a `UDP_SEGMENT` control message. `Ok(true)` if
1840 /// sent (or paced through), `Ok(false)` if the kernel rejected GSO so
1841 /// the caller can fall back.
1842 #[cfg(target_os = "linux")]
1843 fn send_gso_buf(&self, fd: libc::c_int, buf: &[u8], seg_size: u16) -> io::Result<bool> {
1844 const UDP_SEGMENT: libc::c_int = 103;
1845 let mut iov = libc::iovec {
1846 iov_base: buf.as_ptr() as *mut libc::c_void,
1847 iov_len: buf.len(),
1848 };
1849 let mut cmsg_space = [0u64; 8]; // 64 B, 8-byte aligned for cmsghdr
1850 // SAFETY: a zeroed msghdr with one iovec and a single UDP_SEGMENT
1851 // cmsg of a `u16`; `iov`/`buf`/`cmsg_space` outlive the sendmsg, and
1852 // CMSG_SPACE(2) <= 64 B so the cmsg fits.
1853 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
1854 msg.msg_iov = &mut iov;
1855 msg.msg_iovlen = 1;
1856 msg.msg_control = cmsg_space.as_mut_ptr() as *mut libc::c_void;
1857 msg.msg_controllen = unsafe { libc::CMSG_SPACE(size_of::<u16>() as u32) } as _;
1858 unsafe {
1859 let cmsg = libc::CMSG_FIRSTHDR(&msg);
1860 (*cmsg).cmsg_level = libc::SOL_UDP;
1861 (*cmsg).cmsg_type = UDP_SEGMENT;
1862 (*cmsg).cmsg_len = libc::CMSG_LEN(size_of::<u16>() as u32) as _;
1863 std::ptr::write_unaligned(libc::CMSG_DATA(cmsg) as *mut u16, seg_size);
1864 }
1865 let mut spins = 0u32;
1866 loop {
1867 // SAFETY: msg points at the live iov + cmsg; fd is the connected
1868 // socket.
1869 let n = unsafe { libc::sendmsg(fd, &msg, 0) };
1870 if n >= 0 {
1871 return Ok(true);
1872 }
1873 let err = io::Error::last_os_error();
1874 match err.raw_os_error() {
1875 // ENOPROTOOPT/EOPNOTSUPP/EINVAL: the kernel does not offer GSO.
1876 // EIO: the kernel offers it but the NIC cannot segment - a virtio
1877 // device with `tx-udp-segmentation` fixed-off returns EIO at send
1878 // time. Both mean "fall back to plain sendmmsg" (the RLC path
1879 // handles the same EIO in flush_gso).
1880 Some(libc::ENOPROTOOPT)
1881 | Some(libc::EOPNOTSUPP)
1882 | Some(libc::EINVAL)
1883 | Some(libc::EIO) => {
1884 return Ok(false);
1885 }
1886 _ => match err.kind() {
1887 io::ErrorKind::WouldBlock => {
1888 spins += 1;
1889 if spins > 20_000 {
1890 return Ok(true);
1891 }
1892 std::thread::sleep(Duration::from_micros(50));
1893 }
1894 io::ErrorKind::ConnectionReset
1895 | io::ErrorKind::ConnectionRefused
1896 | io::ErrorKind::HostUnreachable
1897 | io::ErrorKind::NetworkUnreachable => {
1898 // A pending ICMP error (peer not up, or a TTL-expired
1899 // from our item-14 Trace probes via IP_RECVERR); drop and
1900 // let ARQ / FEC recover, as for the per-datagram send.
1901 return Ok(true);
1902 }
1903 _ => return Err(err),
1904 },
1905 }
1906 }
1907 }
1908
1909 /// UDP USO egress (Windows). The Windows analogue of GSO: groups
1910 /// consecutive same-size datagrams into one buffer of up to 64 segments
1911 /// / 60 KiB and hands each group to `WSASendMsg` with a
1912 /// `UDP_SEND_MSG_SIZE` control message; the kernel segments it into
1913 /// individual wire datagrams (one path through the stack instead of
1914 /// `k+r`). A lone datagram takes the plain paced `send`. If the kernel
1915 /// rejects USO, the rest of the batch falls back to per-datagram sends.
1916 #[cfg(target_os = "windows")]
1917 fn send_uso(&self, pkts: &[Vec<u8>]) -> io::Result<()> {
1918 use std::os::windows::io::AsRawSocket;
1919 // The demux path has no kernel socket handle for USO; send plainly.
1920 if self.sock.as_udp().is_none() {
1921 for p in pkts {
1922 self.sock.send(p)?;
1923 }
1924 return Ok(());
1925 }
1926 let sock = self.sock.as_udp().expect("Udp checked above").as_raw_socket() as usize;
1927 let mut buf: Vec<u8> = Vec::with_capacity(64 * 1500);
1928 let mut i = 0usize;
1929 while i < pkts.len() {
1930 let seg = pkts[i].len();
1931 buf.clear();
1932 let mut j = i;
1933 while j < pkts.len()
1934 && pkts[j].len() == seg
1935 && (j - i) < 64
1936 && buf.len() + seg <= 61440
1937 {
1938 buf.extend_from_slice(&pkts[j]);
1939 j += 1;
1940 }
1941 if j - i <= 1 || seg == 0 || seg > u16::MAX as usize {
1942 self.send(&pkts[i])?;
1943 i += 1;
1944 continue;
1945 }
1946 if !self.send_uso_buf(sock, &buf, seg as u32)? {
1947 // Kernel lacks USO: send the remaining datagrams plainly.
1948 for pkt in &pkts[i..] {
1949 self.send(pkt)?;
1950 }
1951 return Ok(());
1952 }
1953 i = j;
1954 }
1955 Ok(())
1956 }
1957
1958 /// One `WSASendMsg` with a `UDP_SEND_MSG_SIZE` control message. `Ok(true)`
1959 /// if sent (or paced through), `Ok(false)` if the kernel rejected USO so
1960 /// the caller can fall back. Pacing and ICMP-reset handling match
1961 /// [`send`](Self::send).
1962 #[cfg(target_os = "windows")]
1963 fn send_uso_buf(&self, sock: usize, buf: &[u8], seg_size: u32) -> io::Result<bool> {
1964 use windows_sys::Win32::Networking::WinSock::{WSAGetLastError, WSABUF, WSAMSG};
1965 // WSASendMsg is an extension function; load (and cache) its pointer.
1966 // A failed load means USO is unavailable: fall back.
1967 let Some(wsasendmsg) = load_wsasendmsg(sock) else {
1968 return Ok(false);
1969 };
1970 // Stable Windows ABI values, declared locally so the cmsg layout is
1971 // explicit and independent of windows-sys constant typing.
1972 const IPPROTO_UDP: i32 = 17;
1973 const UDP_SEND_MSG_SIZE: i32 = 2;
1974 const SOCKET_ERROR: i32 = -1;
1975 const WSAEINVAL: i32 = 10022;
1976 const WSAEWOULDBLOCK: i32 = 10035;
1977 const WSAEMSGSIZE: i32 = 10040;
1978 const WSAENOPROTOOPT: i32 = 10042;
1979 const WSAECONNRESET: i32 = 10054;
1980 const WSAECONNREFUSED: i32 = 10061;
1981
1982 let mut data = WSABUF {
1983 len: buf.len() as u32,
1984 buf: buf.as_ptr() as *mut u8,
1985 };
1986 // Control buffer holds one WSACMSGHDR + a u32 segment size.
1987 // 64-bit layout: cmsg_len (usize) @0, cmsg_level (i32) @8,
1988 // cmsg_type (i32) @12, WSA_CMSG_DATA @16. WSA_CMSG_LEN(4) = 20,
1989 // WSA_CMSG_SPACE(4) = 24. `[u64; 4]` gives 32 B, 8-byte aligned.
1990 let mut ctrl = [0u64; 4];
1991 let cp = ctrl.as_mut_ptr() as *mut u8;
1992 // SAFETY: cp points at 32 B of 8-aligned scratch; the four writes
1993 // land at offsets 0/8/12/16, all within bounds, matching the
1994 // WSACMSGHDR layout plus its data word.
1995 unsafe {
1996 std::ptr::write_unaligned(cp as *mut usize, 20usize);
1997 std::ptr::write_unaligned(cp.add(8) as *mut i32, IPPROTO_UDP);
1998 std::ptr::write_unaligned(cp.add(12) as *mut i32, UDP_SEND_MSG_SIZE);
1999 std::ptr::write_unaligned(cp.add(16) as *mut u32, seg_size);
2000 }
2001 let msg = WSAMSG {
2002 name: std::ptr::null_mut(),
2003 namelen: 0,
2004 lpBuffers: &mut data,
2005 dwBufferCount: 1,
2006 Control: WSABUF { len: 24, buf: cp },
2007 dwFlags: 0,
2008 };
2009 let mut sent = 0u32;
2010 let mut spins = 0u32;
2011 loop {
2012 // SAFETY: msg points at the live data/ctrl buffers, which outlive
2013 // the call; sock is the connected socket handle; no overlapped
2014 // structure or completion routine.
2015 let rc = unsafe {
2016 wsasendmsg(
2017 sock,
2018 &msg,
2019 0,
2020 &mut sent,
2021 std::ptr::null_mut(),
2022 std::ptr::null(),
2023 )
2024 };
2025 if rc != SOCKET_ERROR {
2026 USO_OFFLOAD.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2027 return Ok(true);
2028 }
2029 // SAFETY: plain thread-local error fetch, no preconditions.
2030 let err = unsafe { WSAGetLastError() };
2031 match err {
2032 // Kernel lacks USO (or rejected the concatenated buffer):
2033 // signal the caller to fall back to per-datagram sends.
2034 WSAEINVAL | WSAENOPROTOOPT | WSAEMSGSIZE => {
2035 USO_FALLBACK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2036 return Ok(false);
2037 }
2038 // Send buffer full: PACE rather than drop (see `send`).
2039 WSAEWOULDBLOCK => {
2040 spins += 1;
2041 if spins > 20_000 {
2042 return Ok(true);
2043 }
2044 std::thread::sleep(Duration::from_micros(50));
2045 }
2046 // ICMP-driven reset / refused: drop and let ARQ / FEC recover.
2047 WSAECONNRESET | WSAECONNREFUSED => return Ok(true),
2048 _ => return Err(io::Error::from_raw_os_error(err)),
2049 }
2050 }
2051 }
2052
2053 /// One-`sendmmsg`-per-batch egress (Linux/FreeBSD). The socket is
2054 /// connected, so each datagram needs only its iovec, no destination.
2055 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
2056 fn send_mmsg(&self, pkts: &[Vec<u8>]) -> io::Result<()> {
2057 use std::os::fd::AsRawFd;
2058 // The demux path has no kernel fd for sendmmsg; send each datagram plainly.
2059 let fd = match self.sock.as_udp() {
2060 Some(u) => u.as_raw_fd(),
2061 None => {
2062 for p in pkts {
2063 self.sock.send(p)?;
2064 }
2065 return Ok(());
2066 }
2067 };
2068 let mut iovecs: Vec<libc::iovec> = pkts
2069 .iter()
2070 .map(|p| libc::iovec {
2071 iov_base: p.as_ptr() as *mut libc::c_void,
2072 iov_len: p.len(),
2073 })
2074 .collect();
2075 let mut msgs: Vec<libc::mmsghdr> = Vec::with_capacity(pkts.len());
2076 for i in 0..pkts.len() {
2077 // SAFETY: a zeroed mmsghdr with only msg_iov / msg_iovlen set
2078 // is a valid scatter-gather send descriptor on a connected
2079 // socket; the iovec it points at lives in `iovecs` for the
2080 // whole call.
2081 let mut hdr: libc::mmsghdr = unsafe { std::mem::zeroed() };
2082 hdr.msg_hdr.msg_iov = iovecs.as_mut_ptr().wrapping_add(i);
2083 hdr.msg_hdr.msg_iovlen = 1 as _;
2084 msgs.push(hdr);
2085 }
2086 let mut sent = 0usize;
2087 let mut spins = 0u32;
2088 while sent < msgs.len() {
2089 let count = (msgs.len() - sent) as MmsgLen;
2090 // SAFETY: msgs[sent..] is `count` valid mmsghdrs whose iovecs
2091 // reference the live `pkts` buffers; fd is the connected socket.
2092 let n = unsafe { libc::sendmmsg(fd, msgs.as_mut_ptr().add(sent), count, 0) };
2093 if n > 0 {
2094 sent += n as usize;
2095 spins = 0;
2096 continue;
2097 }
2098 let err = io::Error::last_os_error();
2099 match err.kind() {
2100 // Send buffer full: PACE rather than drop (see `send`).
2101 io::ErrorKind::WouldBlock => {
2102 spins += 1;
2103 if spins > 20_000 {
2104 return Ok(());
2105 }
2106 std::thread::sleep(Duration::from_micros(50));
2107 }
2108 io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionRefused => {
2109 return Ok(());
2110 }
2111 _ => return Err(err),
2112 }
2113 }
2114 Ok(())
2115 }
2116}
2117
2118/// Receiver half of the reliable-UDP bridge.
2119/// One peer's block-RS decode window, keyed by session epoch: its decoder,
2120/// delivery frontier, NAK history and feedback cadence. The receiver holds one
2121/// per live sender and owns the socket the session sends through.
2122struct RsSession {
2123 sock: std::sync::Arc<crate::dgram::DgramSock>,
2124 dec: Decoder,
2125 /// When a datagram last arrived, driving [`PEER_SILENCE_TIMEOUT`].
2126 last_data_at: Instant,
2127 peer: Option<SocketAddr>,
2128 /// Count of datagrams actually read off the socket (telemetry; lets
2129 /// a caller distinguish "no packets arriving" from "packets arrive
2130 /// but do not decode/deliver").
2131 recv_count: u64,
2132 /// Per-block time of last NAK, to rate-limit re-requests of each gap
2133 /// to ~one per RTT while still NAKing every gap in parallel. Pruned
2134 /// below the delivery frontier each cycle.
2135 nak_history: BTreeMap<u32, Instant>,
2136 /// When the last plain ACK feedback packet was sent, to rate-limit ACKs.
2137 last_feedback: Instant,
2138 /// Bidirectional control-plane loss accounting. `ctrl_out` counts feedback
2139 /// control packets sent, `ctrl_recv` counts heartbeat control packets
2140 /// received, and `peer_acked` is the highest `last_recv_seq` the sender has
2141 /// reported (how many of our feedback packets it received). When our
2142 /// feedback is being lost (`ctrl_out` outruns `peer_acked`) the ACK cadence
2143 /// shortens, so a lost feedback packet does not stall ARQ.
2144 ctrl_out: u32,
2145 ctrl_recv: u32,
2146 peer_acked: u32,
2147 /// `ctrl_out` / `peer_acked` snapshots at the previous heartbeat, so the
2148 /// feedback-loss estimate is a WINDOWED rate (advance of each between
2149 /// heartbeats) rather than a cumulative count - the latter is dominated by
2150 /// the in-flight backlog, which grows with link delay.
2151 ctrl_out_at_last_hb: u32,
2152 peer_acked_at_last_hb: u32,
2153 /// Last computed reverse-path (feedback) loss fraction (diagnostics).
2154 fb_loss_est: f32,
2155 /// WBest available-bandwidth estimator (item 13): measures the dispersion of
2156 /// the sender's probe pairs / train and computes the available bandwidth,
2157 /// reported back in the feedback so the sender can cross-check its passive
2158 /// BtlBw. `wbest_round` is the probe round it is accumulating; a new round id
2159 /// resets it. `wbest_*_kbps` are the latest computed estimate for telemetry.
2160 wbest: crate::wbest_sensor::WBestEstimator,
2161 wbest_round: Option<u8>,
2162 wbest_avail_kbps: u64,
2163 wbest_capacity_kbps: u64,
2164 /// The peer's link class / quality, from the `Link` frame it echoes - so
2165 /// this end knows what kind of link (Wi-Fi / wired / cellular) carries the
2166 /// other end of the path.
2167 peer_link_class: u8,
2168 peer_link_quality: u8,
2169 /// Current ACK cadence, shortened under reverse-path (feedback) loss.
2170 ack_interval: Duration,
2171 /// Test knob: drop this percent of OUTGOING feedback to inject reverse-path
2172 /// loss (the forward-path counterpart is `debug_drop_pct`). Zero normally.
2173 fb_drop_pct: u32,
2174 fb_drop_rng: u64,
2175 /// Test knob: artificial one-way delay on the feedback path, to
2176 /// reproduce a real link's recovery round-trip on loopback. Zero in
2177 /// normal operation. Feedback queues here and releases when due.
2178 fb_delay: Duration,
2179 fb_pending: VecDeque<(Instant, Vec<u8>)>,
2180 /// Max gaps NAK'd per poll cycle. The default re-requests every gap in
2181 /// parallel; setting 1 reproduces serial head-only recovery (one gap
2182 /// per round-trip) for A/B comparison.
2183 nak_batch: usize,
2184 /// Max time a gap (the head block) is held for recovery before it is
2185 /// skipped to unblock the stream. Long by default, so delivery is
2186 /// effectively reliable; a shorter value trades reliability for
2187 /// bounded latency.
2188 max_hold: Duration,
2189 /// The head block being waited on and when it became the head, for the
2190 /// hold-time deadline.
2191 head_block: u32,
2192 head_since: Instant,
2193 /// Monotonic clock origin for heartbeat receive timestamps.
2194 start: Instant,
2195 /// Diagnostic loss injection: drop this percent of received DATA
2196 /// datagrams before decoding, to validate FEC / ARQ on a lossless
2197 /// link (loopback). Zero in normal operation.
2198 debug_drop_pct: u32,
2199 drop_rng: u64,
2200 /// Diagnostic Gilbert-Elliott BURST loss (per-10000 transition probs): in
2201 /// the Bad state every datagram is dropped, `ge_loss_r/10000` returns to
2202 /// Good and `ge_loss_p/10000` enters Bad, giving a mean burst of
2203 /// `10000 / ge_loss_r`. A known bursty channel for the burst-model A/B.
2204 /// `ge_loss_r == 0` disables it.
2205 ge_loss_p: u32,
2206 ge_loss_r: u32,
2207 ge_bad: bool,
2208 /// Diagnostic WHOLE-block loss: drop every shard of any data block
2209 /// whose id is a multiple of this (0 = off). Such a block cannot be
2210 /// ARQ-recovered (its retransmits are dropped too), so it isolates
2211 /// tower recovery. Outer-parity blocks are never dropped.
2212 drop_block_mod: u32,
2213 /// Diagnostic loss BURST: drop every data datagram whose arrival index
2214 /// falls in `[burst_at, burst_at + burst_len)` - one concentrated loss
2215 /// event, to show a throughput blip and its full recovery in the trace.
2216 /// Zero length = off.
2217 burst_at: u64,
2218 burst_len: u64,
2219 /// Whether the socket is connected to this session's peer. Set only while
2220 /// the receiver holds one session; feedback then rides `send()`, since BSD
2221 /// rejects `send_to()` on a connected socket with EISCONN.
2222 connected: bool,
2223 /// Reused receive buffers for the batched `recvmmsg` path.
2224 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
2225 rbufs: Vec<Vec<u8>>,
2226 /// Whether `UDP_GRO` took on the socket (Linux): coalesced super-buffers
2227 /// read via `recvmsg` and split by the GRO segment size, the receive-side
2228 /// counterpart of GSO. Unset on an old kernel, which keeps `recvmmsg`.
2229 #[cfg(target_os = "linux")]
2230 gro_on: bool,
2231 /// 64 KiB buffer for one coalesced GRO read (Linux).
2232 #[cfg(target_os = "linux")]
2233 gro_buf: Vec<u8>,
2234 /// Most recent IP TTL observed on an inbound datagram (0 = none yet), read
2235 /// from the per-packet cmsg. Echoed to the sender in a `Path` frame so its
2236 /// controller sees hop-count shifts.
2237 last_ttl: u8,
2238 /// Most recent IP TOS byte observed (its low two bits are the ECN field).
2239 last_tos: u8,
2240 /// AccECN (item 15) cumulative counts of the peer's CE-marked and ECN-capable
2241 /// packets, echoed in the `Path` frame so the sender derives a graded CE rate
2242 /// from the deltas (an AQM marks CE before it tail-drops).
2243 ce_count: u64,
2244 ect_count: u64,
2245 /// Sprout-style forecast (item 16): the arrival-rate Kalman filter, the bytes
2246 /// received since the last forecast tick, and when that tick was. The
2247 /// 5th-percentile next-tick forecast is echoed to the sender in a `Forecast`
2248 /// frame so it pre-sizes ahead of a dip.
2249 forecast: crate::forecast_sensor::ArrivalForecast,
2250 fc_bytes: u64,
2251 fc_last: Instant,
2252 /// LEO handover-cadence detector (item 17): autocorrelates the heartbeat OWD
2253 /// trace for a periodic delay spike and reports the period + seconds-to-next
2254 /// in a `Periodicity` frame, so the sender pre-arms one cycle ahead.
2255 periodicity: crate::periodicity_sensor::PeriodicitySensor,
2256 /// The peer's last reported path MTU (from its `Pmtu` frame), 0 = none yet.
2257 peer_pmtu: u16,
2258 /// This host's egress path MTU, set by the receiver each poll and echoed to
2259 /// the sender in a `Pmtu` frame. 0 = unknown.
2260 local_pmtu: u16,
2261}
2262
2263/// Receiver side of the block-RS code. Owns the socket and the drain, and
2264/// routes each datagram to the `RsSession` holding its session epoch.
2265///
2266/// Point-to-point by default: the socket connects to its one peer and reads
2267/// through the GRO / `recvmmsg` / `WSARecvMsg` fast paths.
2268/// [`with_multi_peer`](Self::with_multi_peer) keeps it unconnected and reads
2269/// per datagram with the source captured.
2270pub struct ReliableUdpReceiver {
2271 sock: std::sync::Arc<crate::dgram::DgramSock>,
2272 /// Live decode windows by session epoch, with `order` holding the epochs in
2273 /// first-seen order; sessions are serviced in that order.
2274 sessions: HashMap<u32, RsSession>,
2275 order: Vec<u32>,
2276 /// Epochs under admission challenge, `epoch -> (addr, nonce, sent_at)`.
2277 pending_admissions: HashMap<u32, (SocketAddr, u64, Instant)>,
2278 /// Ceiling on live windows and on candidates under challenge. `None` is
2279 /// unbounded; set by [`with_session_ceiling`](Self::with_session_ceiling).
2280 session_ceiling: Option<usize>,
2281 session_refusals: u64,
2282 /// Monotonic nonce source, mixed so the emitted value is not a guessable
2283 /// counter.
2284 session_nonce_seq: u64,
2285 session_changed: bool,
2286 session_admissions: u64,
2287 session_admission_failures: u64,
2288 start: Instant,
2289 /// Active OS path-event observer (this end's route / carrier / MTU
2290 /// watcher). Reports its egress MTU to the peer in a `Pmtu` frame; its
2291 /// event count is the proof a real path event fired on this host. One per
2292 /// receiver: it watches this host's routes, not a peer.
2293 net_events: NetEventObserver,
2294 /// Serve several peers: socket left unconnected, every datagram read
2295 /// singly and routed by its epoch. A connected socket accepts one address,
2296 /// so this is what admits any peer past the first.
2297 multi_peer: bool,
2298 /// Highest path shift this end's observer has reported, sampled each poll.
2299 /// The live shift decays within seconds; this is peak-held.
2300 net_event_shift_peak: f32,
2301 /// Configuration captured by the builder methods before any peer is seen,
2302 /// stamped onto each session as it opens.
2303 cfg: RsSessionConfig,
2304}
2305
2306/// Receiver settings captured before any peer exists, copied into each session
2307/// as it opens.
2308#[derive(Clone, Copy)]
2309struct RsSessionConfig {
2310 max_hold: Duration,
2311 fb_delay: Duration,
2312 nak_batch: usize,
2313 debug_drop_pct: u32,
2314 drop_rng: u64,
2315 ge_loss_p: u32,
2316 ge_loss_r: u32,
2317 drop_block_mod: u32,
2318 burst_at: u64,
2319 burst_len: u64,
2320 fb_drop_pct: u32,
2321 fb_drop_rng: u64,
2322}
2323
2324/// Enable `UDP_GRO` on a connected receive socket so the kernel coalesces
2325/// consecutive same-size datagrams into one `recvmsg`. Returns whether the
2326/// option took (false on kernels without GRO, where the caller keeps the
2327/// per-datagram `recvmmsg` path).
2328#[cfg(target_os = "linux")]
2329fn enable_gro(sock: &UdpSocket) -> bool {
2330 use std::os::fd::AsRawFd;
2331 const UDP_GRO: libc::c_int = 104;
2332 let on: libc::c_int = 1;
2333 // SAFETY: setsockopt on a valid fd with an int-sized option value that
2334 // outlives the call.
2335 let rc = unsafe {
2336 libc::setsockopt(
2337 sock.as_raw_fd(),
2338 libc::SOL_UDP,
2339 UDP_GRO,
2340 &on as *const libc::c_int as *const libc::c_void,
2341 size_of::<libc::c_int>() as libc::socklen_t,
2342 )
2343 };
2344 rc == 0
2345}
2346
2347/// Ask the kernel to deliver each datagram's IP TTL and TOS byte as control
2348/// messages, so the receiver passively observes the peer's hop count and ECN
2349/// markings (no protocol cost). Best-effort: a kernel that refuses either
2350/// option just yields no such cmsg, and the path sensor stays at its defaults.
2351#[cfg(any(target_os = "linux", target_os = "freebsd"))]
2352fn enable_ttl_ecn(sock: &UdpSocket) {
2353 use std::os::fd::AsRawFd;
2354 let fd = sock.as_raw_fd();
2355 let on: libc::c_int = 1;
2356 // SAFETY: setsockopt on a valid fd with an int-sized option value that
2357 // outlives the call.
2358 let set = |opt: libc::c_int| unsafe {
2359 libc::setsockopt(
2360 fd,
2361 libc::IPPROTO_IP,
2362 opt,
2363 &on as *const libc::c_int as *const libc::c_void,
2364 size_of::<libc::c_int>() as libc::socklen_t,
2365 );
2366 };
2367 set(libc::IP_RECVTTL);
2368 set(libc::IP_RECVTOS);
2369}
2370
2371/// Mark this socket's outgoing packets ECN-capable (ECT(0)), so an ECN-enabled
2372/// AQM on the path marks CE under congestion instead of tail-dropping - the
2373/// signal the AccECN counters (item 15) count. Best-effort.
2374#[cfg(any(target_os = "linux", target_os = "freebsd"))]
2375fn set_ect(sock: &UdpSocket) {
2376 use std::os::fd::AsRawFd;
2377 // ECT(0) is the ECN field value 0b10 in the low two bits of the IP TOS byte.
2378 let tos: libc::c_int = 0b10;
2379 // SAFETY: setsockopt on a valid fd with an int-sized value that outlives it.
2380 unsafe {
2381 libc::setsockopt(
2382 sock.as_raw_fd(),
2383 libc::IPPROTO_IP,
2384 libc::IP_TOS,
2385 &tos as *const libc::c_int as *const libc::c_void,
2386 size_of::<libc::c_int>() as libc::socklen_t,
2387 );
2388 }
2389}
2390
2391/// Read a TTL / TOS ancillary value as a single byte. Linux delivers the
2392/// `IP_TTL` cmsg as a 4-byte `int`; the BSDs deliver it as a 1-byte
2393/// `u_char`. Reading by the cmsg's own payload length (an `int` when four
2394/// or more bytes are present, otherwise one byte) yields the same value on
2395/// either platform. The caller passes a pointer the CMSG walk validated.
2396#[cfg(any(target_os = "linux", target_os = "freebsd"))]
2397fn cmsg_scalar_u8(cmsg: *const libc::cmsghdr) -> u8 {
2398 // SAFETY: `cmsg` comes from CMSG_FIRSTHDR / CMSG_NXTHDR, so it points at
2399 // a valid cmsghdr whose payload occupies `cmsg_len - CMSG_LEN(0)` bytes;
2400 // each read below stays inside that payload.
2401 unsafe {
2402 let hdr_len = libc::CMSG_LEN(0) as usize;
2403 // `cmsg_len` is `size_t` on Linux and `socklen_t` on the BSDs; the
2404 // inferred cast widens both to usize without a same-type cast on the
2405 // platform where it is already usize.
2406 let total: usize = (*cmsg).cmsg_len as _;
2407 let payload = total.saturating_sub(hdr_len);
2408 if payload >= size_of::<libc::c_int>() {
2409 let mut v: libc::c_int = 0;
2410 std::ptr::copy_nonoverlapping(
2411 libc::CMSG_DATA(cmsg),
2412 &mut v as *mut libc::c_int as *mut u8,
2413 size_of::<libc::c_int>(),
2414 );
2415 v as u8
2416 } else if payload >= 1 {
2417 let mut b: u8 = 0;
2418 std::ptr::copy_nonoverlapping(libc::CMSG_DATA(cmsg), &mut b, 1);
2419 b
2420 } else {
2421 0
2422 }
2423 }
2424}
2425
2426/// `recv` on a connected socket, also extracting the datagram's IP TTL from the
2427/// `IP_TTL` cmsg (item 14 reverse-hop count). Returns the byte count and the TTL
2428/// when present. Linux / BSD only; elsewhere it is a plain `recv` with no TTL.
2429#[cfg(any(target_os = "linux", target_os = "freebsd"))]
2430fn recv_with_ttl(sock: &UdpSocket, buf: &mut [u8]) -> io::Result<(usize, Option<u8>)> {
2431 use std::mem::zeroed;
2432 use std::os::fd::AsRawFd;
2433 // SAFETY: msghdr and its iov / control buffers are stack locals that live
2434 // across the recvmsg; the cmsg walk uses the kernel-filled control buffer.
2435 unsafe {
2436 let mut iov = libc::iovec {
2437 iov_base: buf.as_mut_ptr() as *mut libc::c_void,
2438 iov_len: buf.len(),
2439 };
2440 let mut cbuf = [0u8; 64];
2441 let mut msg: libc::msghdr = zeroed();
2442 msg.msg_iov = &mut iov;
2443 msg.msg_iovlen = 1;
2444 msg.msg_control = cbuf.as_mut_ptr() as *mut libc::c_void;
2445 msg.msg_controllen = cbuf.len() as _;
2446 let n = libc::recvmsg(sock.as_raw_fd(), &mut msg, 0);
2447 if n < 0 {
2448 return Err(io::Error::last_os_error());
2449 }
2450 let mut ttl = None;
2451 let mut cmsg = libc::CMSG_FIRSTHDR(&msg);
2452 while !cmsg.is_null() {
2453 if (*cmsg).cmsg_level == libc::IPPROTO_IP
2454 && ((*cmsg).cmsg_type == libc::IP_TTL || (*cmsg).cmsg_type == libc::IP_RECVTTL)
2455 {
2456 ttl = Some(cmsg_scalar_u8(cmsg));
2457 }
2458 cmsg = libc::CMSG_NXTHDR(&msg, cmsg);
2459 }
2460 Ok((n as usize, ttl))
2461 }
2462}
2463
2464#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
2465fn recv_with_ttl(sock: &UdpSocket, buf: &mut [u8]) -> io::Result<(usize, Option<u8>)> {
2466 sock.recv(buf).map(|n| (n, None))
2467}
2468
2469/// Ask the Windows stack to deliver each datagram's IP hop limit (TTL) and
2470/// TOS / ECN as control messages on `WSARecvMsg`, so the receiver passively
2471/// observes the peer's hop count and ECN markings. Mirrors the IPv4 path of
2472/// the Windows reference stack (msquic): `IP_HOPLIMIT` + `IP_RECVTOS` +
2473/// `IP_ECN`, each best-effort - a build that refuses an option just yields
2474/// no such cmsg and the path sensor keeps its defaults.
2475#[cfg(target_os = "windows")]
2476fn enable_ttl_ecn_win(sock: &UdpSocket) {
2477 use std::os::windows::io::AsRawSocket;
2478 use windows_sys::Win32::Networking::WinSock::{
2479 setsockopt, IPPROTO_IP, IP_ECN, IP_HOPLIMIT, IP_RECVTOS,
2480 };
2481 let s = sock.as_raw_socket() as usize;
2482 let on: i32 = 1;
2483 // SAFETY: setsockopt on a valid socket with an int-sized option value
2484 // that outlives the call; return code ignored (best-effort).
2485 let set = |opt: i32| unsafe {
2486 setsockopt(
2487 s,
2488 IPPROTO_IP,
2489 opt,
2490 &on as *const i32 as *const u8,
2491 size_of::<i32>() as i32,
2492 );
2493 };
2494 set(IP_HOPLIMIT);
2495 set(IP_RECVTOS);
2496 set(IP_ECN);
2497}
2498
2499/// Whether the GRO receive path is wanted (default on). `SUBETHA_GRO=0`
2500/// keeps the per-datagram `recvmmsg` path for the A/B baseline. Cached.
2501#[cfg(target_os = "linux")]
2502fn gro_wanted() -> bool {
2503 static EN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2504 *EN.get_or_init(|| std::env::var("SUBETHA_GRO").map(|v| v != "0").unwrap_or(true))
2505}
2506
2507/// Count of `recvmsg` calls on the GRO path.
2508#[cfg(target_os = "linux")]
2509static GRO_RECVMSG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2510/// Count of individual datagrams split out of coalesced GRO super-buffers.
2511#[cfg(target_os = "linux")]
2512static GRO_SEGMENTS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2513
2514/// Process-wide GRO telemetry as `(recvmsg_calls, segments_delivered)`. When
2515/// segments greatly exceeds calls, the kernel coalesced many wire datagrams
2516/// per syscall - the receive-side win. Linux-only; `(0, 0)` elsewhere.
2517pub fn gro_stats() -> (u64, u64) {
2518 #[cfg(target_os = "linux")]
2519 {
2520 use std::sync::atomic::Ordering::Relaxed;
2521 (GRO_RECVMSG.load(Relaxed), GRO_SEGMENTS.load(Relaxed))
2522 }
2523 #[cfg(not(target_os = "linux"))]
2524 {
2525 (0, 0)
2526 }
2527}
2528
2529impl RsSession {
2530 /// A decode window over the receiver's socket, configured from `cfg`.
2531 fn new(sock: std::sync::Arc<crate::dgram::DgramSock>, cfg: RsSessionConfig) -> Self {
2532 Self {
2533 sock,
2534 dec: Decoder::new(),
2535 last_data_at: Instant::now(),
2536 peer: None,
2537 recv_count: 0,
2538 nak_history: BTreeMap::new(),
2539 last_feedback: Instant::now(),
2540 ctrl_out: 0,
2541 ctrl_recv: 0,
2542 peer_acked: 0,
2543 ctrl_out_at_last_hb: 0,
2544 peer_acked_at_last_hb: 0,
2545 fb_loss_est: 0.0,
2546 wbest: crate::wbest_sensor::WBestEstimator::new(BW_PROBE_BYTES),
2547 wbest_round: None,
2548 wbest_avail_kbps: 0,
2549 wbest_capacity_kbps: 0,
2550 peer_link_class: 0,
2551 peer_link_quality: 0,
2552 ack_interval: ACK_INTERVAL,
2553 fb_drop_pct: cfg.fb_drop_pct,
2554 fb_drop_rng: cfg.fb_drop_rng,
2555 fb_delay: cfg.fb_delay,
2556 fb_pending: VecDeque::new(),
2557 nak_batch: cfg.nak_batch,
2558 max_hold: cfg.max_hold,
2559 head_block: 0,
2560 head_since: Instant::now(),
2561 start: Instant::now(),
2562 debug_drop_pct: cfg.debug_drop_pct,
2563 drop_rng: cfg.drop_rng,
2564 ge_loss_p: cfg.ge_loss_p,
2565 ge_loss_r: cfg.ge_loss_r,
2566 ge_bad: false,
2567 drop_block_mod: cfg.drop_block_mod,
2568 burst_at: cfg.burst_at,
2569 burst_len: cfg.burst_len,
2570 connected: false,
2571 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
2572 rbufs: Vec::new(),
2573 #[cfg(target_os = "linux")]
2574 gro_on: false,
2575 #[cfg(target_os = "linux")]
2576 gro_buf: Vec::new(),
2577 last_ttl: 0,
2578 last_tos: 0,
2579 ce_count: 0,
2580 ect_count: 0,
2581 forecast: crate::forecast_sensor::ArrivalForecast::new(),
2582 fc_bytes: 0,
2583 fc_last: Instant::now(),
2584 periodicity: crate::periodicity_sensor::PeriodicitySensor::new(),
2585 peer_pmtu: 0,
2586 local_pmtu: 0,
2587 }
2588 }
2589
2590 /// Datagrams read off the socket so far (telemetry).
2591 pub fn recv_count(&self) -> u64 {
2592 self.recv_count
2593 }
2594
2595 /// Peak loss estimate (0..=255) the decoder has reported (telemetry).
2596 pub fn peak_loss_x255(&self) -> u8 {
2597 self.dec.peak_loss_x255()
2598 }
2599
2600 /// Count of D-SACK false recoveries the decoder's reordering guard detected:
2601 /// spurious retransmissions whose reordered original later arrived. A
2602 /// nonzero value on a reorder-carrying link is the guard firing on the wire.
2603 pub fn false_recovery_count(&self) -> u64 {
2604 self.dec.false_recovery_count()
2605 }
2606
2607 /// Drive the reported burstiness from the Gilbert-Elliott burst model (a
2608 /// real mean burst length) instead of the jitter heuristic - the A/B knob.
2609 pub fn set_ge_burst(&mut self, on: bool) {
2610 self.dec.set_ge_burst(on);
2611 }
2612
2613 /// Fitted mean burst length from the Gilbert-Elliott model, or -1 before
2614 /// the fit converges (telemetry / A/B).
2615 pub fn mean_burst_len(&self) -> f32 {
2616 self.dec.mean_burst_len()
2617 }
2618
2619 /// Estimated clock skew and the skew-corrected OWD trend the controller
2620 /// consumes - the raw trend minus the skew (telemetry).
2621 pub fn owd_skew(&self) -> f64 {
2622 self.dec.owd_skew()
2623 }
2624
2625 pub fn owd_trend_debiased(&self) -> f64 {
2626 self.dec.owd_trend_debiased()
2627 }
2628
2629 /// The current ACK cadence (telemetry); shortens under reverse-path loss.
2630 pub fn ack_interval(&self) -> Duration {
2631 self.ack_interval
2632 }
2633
2634 /// Recompute the ACK cadence from reverse-path (feedback) loss: the share of
2635 /// our feedback the sender has not acknowledged receiving, beyond the normal
2636 /// in-flight. When our feedback is being lost, shorten the cadence so a lost
2637 /// ACK does not stall ARQ; restore it when feedback gets through. `ctrl_out -
2638 /// peer_acked` is feedback in flight plus lost; the sender reports
2639 /// `peer_acked` only on its ~20ms heartbeat cadence, so a steady backlog of
2640 /// a few dozen is normal in-flight and only a fraction well above it is loss.
2641 fn update_feedback_cadence(&mut self) {
2642 // Windowed loss rate over this heartbeat interval: how much feedback we
2643 // sent (`d_out`) versus how much more the sender acknowledged receiving
2644 // (`d_peer`). The cumulative in-flight backlog cancels, so this reflects
2645 // CURRENT reverse-path loss independent of link delay.
2646 let d_out = self.ctrl_out.saturating_sub(self.ctrl_out_at_last_hb);
2647 let d_peer = self.peer_acked.saturating_sub(self.peer_acked_at_last_hb);
2648 self.ctrl_out_at_last_hb = self.ctrl_out;
2649 self.peer_acked_at_last_hb = self.peer_acked;
2650 // Need enough feedback in the window for a stable ratio.
2651 if d_out >= 10 {
2652 let fb_loss = d_out.saturating_sub(d_peer) as f32 / d_out as f32;
2653 self.fb_loss_est = fb_loss;
2654 self.ack_interval = if fb_loss > 0.2 {
2655 ACK_INTERVAL / 4
2656 } else {
2657 ACK_INTERVAL
2658 };
2659 }
2660 }
2661
2662 /// Last computed reverse-path (feedback) loss fraction the receiver measured
2663 /// from the sender's `LossAcct` reports (diagnostics).
2664 pub fn feedback_loss_est(&self) -> f32 {
2665 self.fb_loss_est
2666 }
2667
2668 /// The peer's `(link_class, quality)` from the `Link` frame it echoes
2669 /// (class code: 0 unknown, 1 loopback, 2 wired, 3 Wi-Fi, 4 cellular).
2670 pub fn peer_link(&self) -> (u8, u8) {
2671 (self.peer_link_class, self.peer_link_quality)
2672 }
2673
2674 /// AccECN (item 15) cumulative counts of the peer's CE-marked and ECN-capable
2675 /// packets this receiver has observed. A nonzero `ect` confirms the sender's
2676 /// ECT marking reached us; a rising `ce` is the AQM's congestion signal.
2677 pub fn accecn_counts(&self) -> (u64, u64) {
2678 (self.ce_count, self.ect_count)
2679 }
2680
2681 /// The receiver's current Sprout forecast (item 16): the 5th-percentile
2682 /// next-tick deliverable rate it predicts (bits/s).
2683 pub fn forecast_bps(&self) -> u64 {
2684 (self.forecast.forecast_bps() * 8.0) as u64
2685 }
2686
2687 /// The detected LEO handover cadence (item 17): `(period_s, confidence,
2688 /// secs_to_next_spike)`, or `None` until a periodic delay cadence is found.
2689 pub fn leo_cadence(&self) -> Option<(f64, f64, f64)> {
2690 let (period, conf) = self.periodicity.detected_period()?;
2691 Some((period, conf, self.periodicity.secs_to_next_spike().unwrap_or(0.0)))
2692 }
2693
2694 /// The peer's (sender's) last reported path MTU in bytes (0 = none yet),
2695 /// from its `Pmtu` frame (telemetry).
2696 pub fn peer_pmtu(&self) -> u16 {
2697 self.peer_pmtu
2698 }
2699
2700 /// Diagnostic snapshot of the block blocking in-order delivery:
2701 /// `(block_id, received_shards, k, decoded)`, or `None` if unseen.
2702 pub fn head_status(&self) -> Option<(u32, u32, usize, bool)> {
2703 self.dec.head_status()
2704 }
2705
2706 /// Change the diagnostic loss rate at runtime (0 disables). Lets a test
2707 /// flip a clean link to lossy mid-stream to exercise the controller's
2708 /// re-arm and the ARQ floor on blocks that shipped at Passthrough.
2709 pub fn set_debug_loss(&mut self, pct: u32) {
2710 self.debug_drop_pct = pct.min(100);
2711 }
2712
2713 /// `true` if this datagram belongs to a whole-block-dropped data
2714 /// block (a data block whose id is a multiple of `drop_block_mod`).
2715 /// Outer-parity datagrams are never dropped.
2716 fn drop_whole_block(&self, buf: &[u8]) -> bool {
2717 if self.drop_block_mod == 0 || buf.len() < 5 || is_outer_datagram(buf) {
2718 return false;
2719 }
2720 let bid = u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]);
2721 bid.is_multiple_of(self.drop_block_mod)
2722 }
2723
2724 /// `true` while the receiver is inside a configured loss-burst window
2725 /// (by datagram arrival index). `recv_count` is incremented before the
2726 /// drop checks, so it is the current datagram's 1-based index.
2727 #[inline]
2728 fn in_burst(&self) -> bool {
2729 self.burst_len != 0
2730 && self.recv_count >= self.burst_at
2731 && self.recv_count < self.burst_at + self.burst_len
2732 }
2733
2734 #[inline]
2735 fn roll_drop(&mut self) -> bool {
2736 // Gilbert-Elliott burst loss: drop only in the Bad state, then advance
2737 // the two-state chain. Mean burst = 10000 / ge_loss_r.
2738 if self.ge_loss_r > 0 {
2739 let drop = self.ge_bad;
2740 self.drop_rng = self
2741 .drop_rng
2742 .wrapping_mul(6364136223846793005)
2743 .wrapping_add(1442695040888963407);
2744 let roll = ((self.drop_rng >> 33) as u32) % 10000;
2745 if self.ge_bad {
2746 if roll < self.ge_loss_r {
2747 self.ge_bad = false;
2748 }
2749 } else if roll < self.ge_loss_p {
2750 self.ge_bad = true;
2751 }
2752 return drop;
2753 }
2754 if self.debug_drop_pct == 0 {
2755 return false;
2756 }
2757 self.drop_rng = self
2758 .drop_rng
2759 .wrapping_mul(6364136223846793005)
2760 .wrapping_add(1442695040888963407);
2761 ((self.drop_rng >> 33) as u32) % 100 < self.debug_drop_pct
2762 }
2763
2764 /// Demux-path receive: the unified endpoint's demux reader has already
2765 /// classified datagrams onto this receiver's queue, so there is no kernel
2766 /// fd for the batched recvmmsg / WSARecvMsg path. Pop the queue and process
2767 /// each datagram. Returns `true` when nothing was queued (idle), the same
2768 /// "nothing arrived" convention the fd recv paths use.
2769 fn recv_demux_drain(&mut self, out: &mut Vec<Vec<u8>>) -> io::Result<bool> {
2770 let mut buf = [0u8; RECV_BUF];
2771 let mut idle = true;
2772 loop {
2773 match self.sock.recv_from(&mut buf) {
2774 Ok((n, src)) => {
2775 // Keep the source: on a shared socket this is the only
2776 // place it is observed, and feedback and the session
2777 // challenge are both addressed back to it.
2778 self.peer = Some(src);
2779 self.process_datagram(&buf[..n], out);
2780 idle = false;
2781 }
2782 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
2783 Err(e) => return Err(e),
2784 }
2785 }
2786 Ok(idle)
2787 }
2788
2789 /// Process one received datagram: a heartbeat feeds the timing
2790 /// estimator; the injected-loss filters swallow it; otherwise it is
2791 /// decoded and any newly deliverable items are appended to `out`.
2792 fn process_datagram(&mut self, buf: &[u8], out: &mut Vec<Vec<u8>>) {
2793 self.recv_count += 1;
2794 self.last_data_at = Instant::now();
2795 if self.roll_drop() {
2796 return;
2797 }
2798 if is_control(buf) {
2799 if let Some(cp) = decode_control(buf) {
2800 // A control packet from the sender (a heartbeat). Count it for
2801 // reverse-path loss accounting, and read its LossAcct to learn
2802 // how many of OUR feedback packets the sender has received.
2803 self.ctrl_recv = self.ctrl_recv.wrapping_add(1);
2804 if let Some(la) = cp.loss_acct
2805 && la.last_recv_seq > self.peer_acked
2806 {
2807 self.peer_acked = la.last_recv_seq;
2808 }
2809 if let Some(lk) = cp.link {
2810 self.peer_link_class = lk.class;
2811 self.peer_link_quality = lk.quality;
2812 }
2813 if let Some(pm) = cp.pmtu
2814 && pm.pmtu != 0
2815 {
2816 self.peer_pmtu = pm.pmtu;
2817 }
2818 // A beat announcing a session this receiver does not hold.
2819 // Recorded, not trusted: it goes through the same challenge
2820 // as an unrecognised data epoch.
2821 if let Some(announced) = cp.session_announce
2822 && self.dec.session_epoch().is_some_and(|e| e != announced)
2823 {
2824 self.dec.note_unknown_epoch(announced);
2825 }
2826 // Session-challenge answers name a candidate epoch and are
2827 // handled by the receiver's drain, not here.
2828 if let Some(t) = cp.timing {
2829 let recv_ts = self.start.elapsed().as_micros() as u64;
2830 self.dec.on_heartbeat(t.send_ts, recv_ts);
2831 // Item 17: feed the relative OWD (recv minus send timestamp -
2832 // the constant clock offset cancels in the autocorrelation's
2833 // mean subtraction) to the LEO cadence detector.
2834 let owd = recv_ts as f64 - t.send_ts as f64;
2835 self.periodicity.observe(owd, recv_ts);
2836 }
2837 if !cp.bw_probe.is_empty() {
2838 // Sub-microsecond arrival so a small dispersion at a high
2839 // capacity is still resolved.
2840 let arrival_us = self.start.elapsed().as_nanos() as f64 / 1000.0;
2841 self.ingest_bw_probe(&cp.bw_probe, arrival_us);
2842 }
2843 self.update_feedback_cadence();
2844 }
2845 } else if self.drop_whole_block(buf) {
2846 // Whole-block loss injection: swallow it.
2847 } else if self.in_burst() {
2848 // Loss-burst injection: swallow it.
2849 } else {
2850 // AccECN (item 15): count this data packet's ECN. An ECN-capable
2851 // packet (ECT0 / ECT1 / CE) advances ect_count; a CE mark advances
2852 // ce_count - the AQM's congestion signal, which it sets before it
2853 // tail-drops. Echoed cumulatively in the Path frame.
2854 let ecn = self.last_tos & 0b11;
2855 if ecn != 0 {
2856 self.ect_count += 1;
2857 if ecn == crate::path_sensor::ECN_CE {
2858 self.ce_count += 1;
2859 }
2860 }
2861 // Item 16: this data datagram's bytes are an arrival the Sprout
2862 // forecaster integrates over the tick (the path's deliverable rate).
2863 self.fc_bytes += buf.len() as u64;
2864 // Stamp the arrival so the decoder's loss differentiator measures
2865 // shard inter-arrival (the Biaz input); the clock origin is shared
2866 // with the heartbeat OWD above.
2867 let recv_us = self.start.elapsed().as_micros() as u64;
2868 out.extend(self.dec.on_packet_at(buf, recv_us));
2869 }
2870 }
2871
2872 /// Run one Sprout forecast tick if `FORECAST_TICK` has elapsed: feed the
2873 /// bytes received since the last tick over that interval, then reset the
2874 /// accumulator. The forecast itself is read in the feedback build.
2875 fn maybe_observe_forecast(&mut self) {
2876 let dt = self.fc_last.elapsed();
2877 if dt >= FORECAST_TICK {
2878 self.forecast.observe(self.fc_bytes, dt.as_secs_f64());
2879 self.fc_bytes = 0;
2880 self.fc_last = Instant::now();
2881 }
2882 }
2883
2884 /// Feed the WBest estimator one probe datagram's frame at its arrival time.
2885 /// A new round id resets the estimator; pair probes (`idx < 2*pairs`) and
2886 /// train probes (the rest) are routed by index. Recomputes the estimate
2887 /// (kbit/s) once both stages have samples.
2888 fn ingest_bw_probe(&mut self, probes: &[crate::control_frame::BwProbeFrame], arrival_us: f64) {
2889 let pair_probes = 2 * BW_PROBE_PAIRS;
2890 for f in probes {
2891 if self.wbest_round != Some(f.probe_id) {
2892 self.wbest.reset();
2893 self.wbest_round = Some(f.probe_id);
2894 }
2895 if f.idx < pair_probes {
2896 self.wbest.on_pair_probe(f.idx % 2, arrival_us);
2897 } else {
2898 self.wbest.on_train_probe(arrival_us);
2899 }
2900 }
2901 if let Some(c) = self.wbest.effective_capacity_bps() {
2902 self.wbest_capacity_kbps = (c / 1000.0) as u64;
2903 }
2904 if let Some(a) = self.wbest.available_bps() {
2905 self.wbest_avail_kbps = (a / 1000.0) as u64;
2906 }
2907 }
2908
2909 /// The WBest estimate this receiver has computed: (available bandwidth,
2910 /// effective capacity) in bits/s, both 0 until a probe round completes.
2911 pub fn wbest_bps(&self) -> (u64, u64) {
2912 (self.wbest_avail_kbps * 1000, self.wbest_capacity_kbps * 1000)
2913 }
2914
2915 /// Read datagrams into `out`. On Linux/FreeBSD, once the peer is known
2916 /// the socket is connected and a whole burst is read in one `recvmmsg`
2917 /// syscall - the per-datagram `recvfrom` was a top kernel cost on the
2918 /// receiver. The first datagram and other platforms use a single
2919 /// `recv_from`. Returns `true` when no data arrived (timeout park).
2920 fn recv_into(&mut self, out: &mut Vec<Vec<u8>>) -> io::Result<bool> {
2921 #[cfg(target_os = "linux")]
2922 if self.connected {
2923 // GRO coalesces a whole burst into one skb; fall back to the
2924 // per-datagram recvmmsg batch on kernels without GRO.
2925 if self.gro_on {
2926 return self.recv_gro(out);
2927 }
2928 return self.recv_batch(out);
2929 }
2930 // Gated on `connected`, not on having a peer: the fast paths read
2931 // an associated socket, and `release_silent_peer` dissolves that
2932 // association while leaving `peer` set as the last address known.
2933 #[cfg(target_os = "freebsd")]
2934 if self.connected {
2935 return self.recv_batch(out);
2936 }
2937 #[cfg(target_os = "windows")]
2938 if self.connected {
2939 return self.recv_wsamsg(out);
2940 }
2941 let mut buf = [0u8; RECV_BUF];
2942 match self.sock.recv_from(&mut buf) {
2943 Ok((n, src)) => {
2944 self.peer = Some(src);
2945 // Connect to the peer (the transport is point-to-point) so
2946 // the batched path needs no per-datagram source capture.
2947 // Reached only on the first datagram on Linux/FreeBSD;
2948 // best-effort, since recvmmsg works unconnected too.
2949 #[cfg(target_os = "linux")]
2950 {
2951 self.connected = self.sock.connect(src).is_ok();
2952 // Turn on GRO now the socket is connected; the next poll
2953 // reads coalesced super-buffers. `SUBETHA_GRO=0` keeps
2954 // the recvmmsg path for the A/B baseline.
2955 self.gro_on = self.connected
2956 && gro_wanted()
2957 && self.sock.as_udp().map(enable_gro).unwrap_or(false);
2958 }
2959 #[cfg(target_os = "freebsd")]
2960 {
2961 self.connected = self.sock.connect(src).is_ok();
2962 }
2963 #[cfg(target_os = "windows")]
2964 {
2965 // Connect so WSARecvMsg reads from the peer with no source
2966 // capture and feedback rides send() like the connected
2967 // Unix paths.
2968 self.connected = self.sock.connect(src).is_ok();
2969 }
2970 self.process_datagram(&buf[..n], out);
2971 Ok(false)
2972 }
2973 Err(e)
2974 if matches!(
2975 e.kind(),
2976 io::ErrorKind::WouldBlock
2977 | io::ErrorKind::TimedOut
2978 | io::ErrorKind::ConnectionReset
2979 | io::ErrorKind::ConnectionRefused
2980 ) =>
2981 {
2982 // The ICMP-port-unreachable artifact on a connected UDP
2983 // socket - ConnectionReset on Windows, ConnectionRefused on
2984 // Linux/BSD; treat it like a timeout.
2985 Ok(true)
2986 }
2987 Err(e) => Err(e),
2988 }
2989 }
2990
2991 /// Walk one received message's control buffer for the IP TTL and TOS
2992 /// cmsgs requested by [`enable_ttl_ecn`], updating `last_ttl` /
2993 /// `last_tos` so the next `Path` frame echoes them (the TOS byte's low
2994 /// two bits are the ECN field). FreeBSD may tag the TTL with cmsg type
2995 /// `IP_RECVTTL` and Linux with `IP_TTL`; both spellings are accepted.
2996 /// Used by the per-datagram `recvmmsg` batch path; the GRO path inlines
2997 /// the same read alongside its segment-size cmsg.
2998 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
2999 fn observe_ttl_tos(&mut self, msg: &libc::msghdr) {
3000 // SAFETY: `msg` is a live msghdr whose `msg_control` the kernel
3001 // filled; the CMSG walk stays within the reported `msg_controllen`,
3002 // and `cmsg_scalar_u8` reads only within each cmsg's payload.
3003 unsafe {
3004 let mut cmsg = libc::CMSG_FIRSTHDR(msg as *const libc::msghdr);
3005 while !cmsg.is_null() {
3006 let level = (*cmsg).cmsg_level;
3007 let cty = (*cmsg).cmsg_type;
3008 if level == libc::IPPROTO_IP
3009 && (cty == libc::IP_TTL || cty == libc::IP_RECVTTL)
3010 {
3011 self.last_ttl = cmsg_scalar_u8(cmsg);
3012 } else if level == libc::IPPROTO_IP
3013 && (cty == libc::IP_TOS || cty == libc::IP_RECVTOS)
3014 {
3015 self.last_tos = cmsg_scalar_u8(cmsg);
3016 }
3017 cmsg = libc::CMSG_NXTHDR(msg as *const libc::msghdr, cmsg);
3018 }
3019 }
3020 }
3021
3022 /// Batched receive: up to `RECV_BATCH` datagrams from the connected
3023 /// socket in one `recvmmsg` syscall. `MSG_WAITFORONE` parks (up to the
3024 /// socket read timeout) for the first datagram, then takes everything
3025 /// else already queued.
3026 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
3027 fn recv_batch(&mut self, out: &mut Vec<Vec<u8>>) -> io::Result<bool> {
3028 use std::os::fd::AsRawFd;
3029 const RECV_BATCH: usize = 32;
3030 // 64 B of cmsg scratch per message: room for the IP_TTL and IP_TOS
3031 // ancillary objects (`CMSG_SPACE(int)` + `CMSG_SPACE(byte)` < 64) so
3032 // each datagram's TTL / ECN lands in its own slot.
3033 const CMSG_WORDS: usize = 8;
3034 if self.rbufs.len() < RECV_BATCH {
3035 self.rbufs.resize_with(RECV_BATCH, || vec![0u8; RECV_BUF]);
3036 }
3037 // The demux path has no kernel fd for recvmmsg; drain its queue plainly.
3038 if self.sock.as_udp().is_none() {
3039 return self.recv_demux_drain(out);
3040 }
3041 let fd = self.sock.as_udp().expect("Udp checked above").as_raw_fd();
3042 let mut iovecs: Vec<libc::iovec> = self
3043 .rbufs
3044 .iter_mut()
3045 .take(RECV_BATCH)
3046 .map(|b| libc::iovec {
3047 iov_base: b.as_mut_ptr() as *mut libc::c_void,
3048 iov_len: RECV_BUF,
3049 })
3050 .collect();
3051 // One cmsg scratch buffer per message; the kernel writes each
3052 // datagram's TTL / TOS ancillary data into its own slot and sets
3053 // that message's `msg_controllen` to the bytes it wrote.
3054 let mut ctrl: Vec<[u64; CMSG_WORDS]> = vec![[0u64; CMSG_WORDS]; RECV_BATCH];
3055 let mut msgs: Vec<libc::mmsghdr> = Vec::with_capacity(RECV_BATCH);
3056 for (i, slot) in ctrl.iter_mut().enumerate() {
3057 // SAFETY: a zeroed mmsghdr with msg_iov / msg_iovlen pointing at
3058 // the live iovec and msg_control / msg_controllen pointing at this
3059 // message's cmsg slot is a valid receive descriptor on a connected
3060 // socket; both buffers outlive the recvmmsg call.
3061 let mut hdr: libc::mmsghdr = unsafe { std::mem::zeroed() };
3062 hdr.msg_hdr.msg_iov = iovecs.as_mut_ptr().wrapping_add(i);
3063 hdr.msg_hdr.msg_iovlen = 1 as _;
3064 hdr.msg_hdr.msg_control = slot.as_mut_ptr() as *mut libc::c_void;
3065 hdr.msg_hdr.msg_controllen = (CMSG_WORDS * size_of::<u64>()) as _;
3066 msgs.push(hdr);
3067 }
3068 // An explicit timeout bounds the wait for the FIRST message. With a
3069 // NULL timeout FreeBSD's recvmmsg blocks until every `vlen` buffer
3070 // fills - MSG_WAITFORONE only sets MSG_DONTWAIT *after* the first
3071 // message, so at end-of-stream the first receive blocks forever
3072 // (FreeBSD does not honor SO_RCVTIMEO here the way Linux does). The
3073 // timeout matches the socket read-timeout park that drives tail-ARQ
3074 // and is equivalent to the SO_RCVTIMEO behavior on Linux.
3075 let mut ts = libc::timespec {
3076 tv_sec: 0,
3077 tv_nsec: 4_000_000,
3078 };
3079 // SAFETY: msgs is RECV_BATCH valid descriptors into the live rbufs;
3080 // fd is the connected socket; ts outlives the call. The pointer is
3081 // `*mut` (Linux) and coerces to `*const` (FreeBSD).
3082 let n = unsafe {
3083 libc::recvmmsg(
3084 fd,
3085 msgs.as_mut_ptr(),
3086 RECV_BATCH as MmsgLen,
3087 libc::MSG_WAITFORONE,
3088 &mut ts as *mut libc::timespec,
3089 )
3090 };
3091 if n == 0 {
3092 // FreeBSD returns 0 when the recvmmsg timeout expires with no
3093 // data; Linux returns -1/EAGAIN. Both mean the read-timeout park,
3094 // which must drive tail-ARQ feedback, NOT surface as an error
3095 // (an error here skips the feedback in poll() and the sender's
3096 // drain_until_acked then waits forever for ACKs that never come).
3097 return Ok(true);
3098 }
3099 if n < 0 {
3100 let e = io::Error::last_os_error();
3101 return match e.kind() {
3102 io::ErrorKind::WouldBlock
3103 | io::ErrorKind::TimedOut
3104 | io::ErrorKind::ConnectionReset
3105 | io::ErrorKind::ConnectionRefused => Ok(true),
3106 _ => Err(e),
3107 };
3108 }
3109 for (i, msg) in msgs.iter().take(n as usize).enumerate() {
3110 let len = msg.msg_len as usize;
3111 if len == 0 || len > RECV_BUF {
3112 continue;
3113 }
3114 // Pull this datagram's TTL / TOS out of its own cmsg slot before
3115 // the decode borrow. recvmmsg set this message's `msg_controllen`
3116 // to the bytes it wrote, so the walk reads only real ancillary
3117 // data.
3118 self.observe_ttl_tos(&msg.msg_hdr);
3119 // Copy out so the decode can take &mut self; the on-decode
3120 // path copies the shard regardless. `i` indexes the parallel
3121 // rbufs slot, copied before the &mut self decode borrow.
3122 let mut tmp = [0u8; RECV_BUF];
3123 tmp[..len].copy_from_slice(&self.rbufs[i][..len]);
3124 self.process_datagram(&tmp[..len], out);
3125 }
3126 Ok(false)
3127 }
3128
3129 /// Coalesced receive (Linux GRO). One `recvmsg` reads a super-buffer of
3130 /// up to 64 KiB that the kernel coalesced from many same-size datagrams;
3131 /// its `UDP_GRO` control message carries the segment size, so the buffer
3132 /// splits back into the individual shards. The first read parks on the
3133 /// socket timeout (so a quiet link still drives tail-ARQ); queued
3134 /// super-buffers are then drained with `MSG_DONTWAIT`. This is the
3135 /// receive-side counterpart of GSO: one skb up the stack instead of
3136 /// `k + r`. Returns `true` only when nothing arrived (timeout park).
3137 #[cfg(target_os = "linux")]
3138 fn recv_gro(&mut self, out: &mut Vec<Vec<u8>>) -> io::Result<bool> {
3139 use std::os::fd::AsRawFd;
3140 use std::sync::atomic::Ordering::Relaxed;
3141 const UDP_GRO: libc::c_int = 104;
3142 const GRO_BUF: usize = 65536;
3143 if self.gro_buf.len() < GRO_BUF {
3144 self.gro_buf.resize(GRO_BUF, 0);
3145 }
3146 if self.sock.as_udp().is_none() {
3147 return self.recv_demux_drain(out);
3148 }
3149 let fd = self.sock.as_udp().expect("Udp checked above").as_raw_fd();
3150 let mut got_any = false;
3151 let mut first = true;
3152 loop {
3153 let mut iov = libc::iovec {
3154 iov_base: self.gro_buf.as_mut_ptr() as *mut libc::c_void,
3155 iov_len: GRO_BUF,
3156 };
3157 // Room for the UDP_GRO cmsg plus the IP_TTL and IP_TOS cmsgs.
3158 let mut cmsg_space = [0u64; 16];
3159 // SAFETY: a zeroed msghdr with one iovec into the live gro_buf
3160 // and a cmsg scratch buffer that outlives the call.
3161 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
3162 msg.msg_iov = &mut iov;
3163 msg.msg_iovlen = 1;
3164 msg.msg_control = cmsg_space.as_mut_ptr() as *mut libc::c_void;
3165 msg.msg_controllen = (cmsg_space.len() * size_of::<u64>()) as _;
3166 let flags = if first { 0 } else { libc::MSG_DONTWAIT };
3167 // SAFETY: fd is the connected socket; msg points at live buffers.
3168 let n = unsafe { libc::recvmsg(fd, &mut msg, flags) };
3169 if n < 0 {
3170 let e = io::Error::last_os_error();
3171 return match e.kind() {
3172 io::ErrorKind::WouldBlock
3173 | io::ErrorKind::TimedOut
3174 | io::ErrorKind::ConnectionReset
3175 | io::ErrorKind::ConnectionRefused => Ok(!got_any),
3176 _ => Err(e),
3177 };
3178 }
3179 let n = n as usize;
3180 // Segment size from the UDP_GRO cmsg; absent = a single datagram.
3181 let mut seg = n;
3182 // SAFETY: msg.msg_control points at the cmsg buffer the kernel
3183 // filled; the CMSG walk stays within the reported msg_controllen.
3184 unsafe {
3185 let mut cmsg = libc::CMSG_FIRSTHDR(&msg);
3186 while !cmsg.is_null() {
3187 let level = (*cmsg).cmsg_level;
3188 let cty = (*cmsg).cmsg_type;
3189 if level == libc::SOL_UDP && cty == UDP_GRO {
3190 let mut s: libc::c_int = 0;
3191 std::ptr::copy_nonoverlapping(
3192 libc::CMSG_DATA(cmsg),
3193 &mut s as *mut libc::c_int as *mut u8,
3194 size_of::<libc::c_int>(),
3195 );
3196 if s > 0 {
3197 seg = s as usize;
3198 }
3199 } else if level == libc::IPPROTO_IP && cty == libc::IP_TTL {
3200 let mut t: libc::c_int = 0;
3201 std::ptr::copy_nonoverlapping(
3202 libc::CMSG_DATA(cmsg),
3203 &mut t as *mut libc::c_int as *mut u8,
3204 size_of::<libc::c_int>(),
3205 );
3206 self.last_ttl = t as u8;
3207 } else if level == libc::IPPROTO_IP && cty == libc::IP_TOS {
3208 // The IP_TOS cmsg is a single byte; its low two bits
3209 // are the ECN field.
3210 let mut tos: u8 = 0;
3211 std::ptr::copy_nonoverlapping(libc::CMSG_DATA(cmsg), &mut tos, 1);
3212 self.last_tos = tos;
3213 }
3214 cmsg = libc::CMSG_NXTHDR(&msg, cmsg);
3215 }
3216 }
3217 if seg == 0 {
3218 seg = n;
3219 }
3220 // Split the coalesced buffer into shards. All segments are `seg`
3221 // bytes except possibly the final remainder.
3222 let mut off = 0usize;
3223 let mut segs = 0u64;
3224 while off < n {
3225 let end = (off + seg).min(n);
3226 let len = end - off;
3227 if len > 0 && len <= RECV_BUF {
3228 let mut tmp = [0u8; RECV_BUF];
3229 tmp[..len].copy_from_slice(&self.gro_buf[off..end]);
3230 self.process_datagram(&tmp[..len], out);
3231 segs += 1;
3232 }
3233 off = end;
3234 }
3235 GRO_RECVMSG.fetch_add(1, Relaxed);
3236 GRO_SEGMENTS.fetch_add(segs, Relaxed);
3237 got_any = true;
3238 first = false;
3239 // Bound the drain so one poll cannot spin without yielding.
3240 if out.len() > 4096 {
3241 return Ok(false);
3242 }
3243 }
3244 }
3245
3246 /// Receive one datagram on Windows via `WSARecvMsg`, reading the IP hop
3247 /// limit and TOS / ECN from its control messages - the Windows analogue
3248 /// of the Linux/FreeBSD cmsg path. The socket is connected to the peer by
3249 /// the time this runs, so no source capture is needed and the read parks
3250 /// on the socket timeout (driving tail-ARQ). Falls back to a plain
3251 /// connected `recv` if the `WSARecvMsg` extension is unavailable. Returns
3252 /// `true` only when nothing arrived (timeout park).
3253 #[cfg(target_os = "windows")]
3254 fn recv_wsamsg(&mut self, out: &mut Vec<Vec<u8>>) -> io::Result<bool> {
3255 use std::os::windows::io::AsRawSocket;
3256 use windows_sys::Win32::Networking::WinSock::{WSAGetLastError, WSABUF, WSAMSG};
3257 if self.sock.as_udp().is_none() {
3258 return self.recv_demux_drain(out);
3259 }
3260 let sock = self.sock.as_udp().expect("Udp checked above").as_raw_socket() as usize;
3261 let Some(wsarecvmsg) = load_wsarecvmsg(sock) else {
3262 // Extension unavailable: plain connected recv, no TTL / ECN cmsg.
3263 let mut buf = [0u8; RECV_BUF];
3264 return match self.sock.recv(&mut buf) {
3265 Ok(n) => {
3266 self.process_datagram(&buf[..n], out);
3267 Ok(false)
3268 }
3269 Err(e)
3270 if matches!(
3271 e.kind(),
3272 io::ErrorKind::WouldBlock
3273 | io::ErrorKind::TimedOut
3274 | io::ErrorKind::ConnectionReset
3275 | io::ErrorKind::ConnectionRefused
3276 ) =>
3277 {
3278 Ok(true)
3279 }
3280 Err(e) => Err(e),
3281 };
3282 };
3283 const SOCKET_ERROR: i32 = -1;
3284 const WSAEMSGSIZE: i32 = 10040;
3285 const WSAEWOULDBLOCK: i32 = 10035;
3286 const WSAETIMEDOUT: i32 = 10060;
3287 const WSAECONNRESET: i32 = 10054;
3288 const WSAECONNREFUSED: i32 = 10061;
3289 let mut buf = [0u8; RECV_BUF];
3290 let mut data = WSABUF {
3291 len: RECV_BUF as u32,
3292 buf: buf.as_mut_ptr(),
3293 };
3294 // Control buffer for the hop-limit + TOS / ECN cmsgs. Each is a 16 B
3295 // WSACMSGHDR + a 4 B int, space-aligned to 24 B; `[u64; 16]` = 128 B
3296 // holds several comfortably.
3297 let mut ctrl = [0u64; 16];
3298 let mut msg = WSAMSG {
3299 name: std::ptr::null_mut(),
3300 namelen: 0,
3301 lpBuffers: &mut data,
3302 dwBufferCount: 1,
3303 Control: WSABUF {
3304 len: (ctrl.len() * size_of::<u64>()) as u32,
3305 buf: ctrl.as_mut_ptr() as *mut u8,
3306 },
3307 dwFlags: 0,
3308 };
3309 let mut recvd = 0u32;
3310 // SAFETY: msg points at the live data / ctrl buffers, which outlive
3311 // the call; sock is the connected socket; no overlapped structure or
3312 // completion routine.
3313 let rc = unsafe {
3314 wsarecvmsg(
3315 sock,
3316 &mut msg,
3317 &mut recvd,
3318 std::ptr::null_mut(),
3319 std::ptr::null(),
3320 )
3321 };
3322 if rc == SOCKET_ERROR {
3323 // SAFETY: plain thread-local error fetch, no preconditions.
3324 let err = unsafe { WSAGetLastError() };
3325 return match err {
3326 // Read-timeout park (drives tail-ARQ), ICMP reset / refused,
3327 // or an over-size datagram: nothing usable this cycle.
3328 WSAEWOULDBLOCK | WSAETIMEDOUT | WSAECONNRESET | WSAECONNREFUSED
3329 | WSAEMSGSIZE => Ok(true),
3330 _ => Err(io::Error::from_raw_os_error(err)),
3331 };
3332 }
3333 let n = recvd as usize;
3334 if n == 0 || n > RECV_BUF {
3335 return Ok(true);
3336 }
3337 // Walk the control buffer the kernel filled (`msg.Control.len` holds
3338 // the bytes written) for the TTL and TOS / ECN cmsgs.
3339 let ctrl_len = (msg.Control.len as usize).min(ctrl.len() * size_of::<u64>());
3340 // SAFETY: `ctrl` holds `ctrl_len` bytes the kernel initialized.
3341 let cbytes = unsafe { std::slice::from_raw_parts(ctrl.as_ptr() as *const u8, ctrl_len) };
3342 self.observe_wsa_cmsgs(cbytes);
3343 self.process_datagram(&buf[..n], out);
3344 Ok(false)
3345 }
3346
3347 /// Walk a `WSARecvMsg` control buffer for the IPv4 hop-limit (`IP_TTL`)
3348 /// and TOS / ECN (`IP_TOS` / `IP_ECN`) cmsgs, updating `last_ttl` /
3349 /// `last_tos` (the TOS byte's low two bits are the ECN field). Each
3350 /// Windows cmsg payload is a 4-byte `int`. The 64-bit `WSACMSGHDR` is
3351 /// `cmsg_len` (usize) at 0, `cmsg_level` (i32) at 8, `cmsg_type` (i32)
3352 /// at 12, and the data at 16 (the header size aligned up to the 8-byte
3353 /// natural alignment).
3354 #[cfg(target_os = "windows")]
3355 fn observe_wsa_cmsgs(&mut self, control: &[u8]) {
3356 use windows_sys::Win32::Networking::WinSock::{IPPROTO_IP, IP_ECN, IP_TOS, IP_TTL};
3357 const HDR: usize = 16;
3358 let lvl_ip = IPPROTO_IP;
3359 let mut off = 0usize;
3360 while off + HDR <= control.len() {
3361 // SAFETY: every read is bounds-checked against control.len()
3362 // before it runs, and `control` holds that many initialized bytes.
3363 let cmsg_len =
3364 unsafe { std::ptr::read_unaligned(control.as_ptr().add(off) as *const usize) };
3365 if cmsg_len < HDR || off + cmsg_len > control.len() {
3366 break;
3367 }
3368 let level =
3369 unsafe { std::ptr::read_unaligned(control.as_ptr().add(off + 8) as *const i32) };
3370 let cty =
3371 unsafe { std::ptr::read_unaligned(control.as_ptr().add(off + 12) as *const i32) };
3372 if level == lvl_ip && cmsg_len - HDR >= size_of::<i32>() {
3373 let val = unsafe {
3374 std::ptr::read_unaligned(control.as_ptr().add(off + HDR) as *const i32)
3375 };
3376 if cty == IP_TTL {
3377 self.last_ttl = val as u8;
3378 } else if cty == IP_TOS || cty == IP_ECN {
3379 self.last_tos = val as u8;
3380 }
3381 }
3382 // Advance to the next header, the cmsg length aligned up to 8.
3383 off += (cmsg_len + 7) & !7;
3384 }
3385 }
3386
3387 /// Receive one datagram (or hit the read timeout), decode it, send
3388 /// feedback to the peer, and return any items that became
3389 /// deliverable in stream order. On timeout, feedback is sent with
3390 /// tail-ARQ drive so a stalled final block recovers.
3391 fn service(&mut self, timed_out: bool) -> io::Result<Vec<Vec<u8>>> {
3392 let mut out = Vec::new();
3393 // Release any delayed feedback whose injected link latency has
3394 // elapsed (no-op unless a feedback delay is configured).
3395 self.flush_delayed_feedback();
3396 if let Some(peer) = self.peer {
3397 let base = self.dec.feedback(timed_out);
3398 let now = Instant::now();
3399 // Plain ACK (cumulative frontier + sensors) on the ACK cadence
3400 // or a timeout drive. The NAK rides the selective pass below,
3401 // so strip it from the ACK packet.
3402 if timed_out || self.last_feedback.elapsed() >= self.ack_interval {
3403 let mut ack = base;
3404 ack.nak_block = NAK_NONE;
3405 ack.nak_mask = 0;
3406 self.queue_feedback(peer, &ack);
3407 self.last_feedback = now;
3408 }
3409 // Selective NAK: re-request EVERY gap the window is holding in
3410 // this one cycle (capped), each rate-limited per-block to ~one
3411 // per RTT. This is the head-of-line fix: retransmits for all
3412 // gaps flow in a single round-trip and the delivery frontier
3413 // advances in bulk, instead of recovering one gap per
3414 // round-trip while the wire stalls behind it.
3415 // After adopting a replacement session the frontier restarts at
3416 // the bottom while nothing above it has been seen, so the gap
3417 // scan finds nothing and the tail drive is the only thing that
3418 // asks for the next block. That drive is normally gated on a
3419 // read timeout, which a peer beating steadily never produces -
3420 // so the receiver would wait for a request it never makes while
3421 // the sender waits to be asked. Drive it whenever the frontier
3422 // is ahead of everything seen, which is exactly that state and
3423 // clears itself as soon as the stream resumes.
3424 let catching_up = self.dec.next_needed() > self.dec.highest_seen();
3425 let gaps = self.dec.missing_blocks(self.nak_batch, timed_out || catching_up);
3426 for (block, mask) in gaps {
3427 if mask == 0 {
3428 continue;
3429 }
3430 let fresh = self
3431 .nak_history
3432 .get(&block)
3433 .is_none_or(|t| now.duration_since(*t) >= NAK_COOLDOWN);
3434 if fresh {
3435 let mut nfb = base;
3436 nfb.nak_block = block;
3437 nfb.nak_mask = mask;
3438 self.queue_feedback(peer, &nfb);
3439 self.nak_history.insert(block, now);
3440 }
3441 }
3442 // Prune per-block NAK history below the delivery frontier; those
3443 // blocks are delivered and will never be NAK'd again.
3444 let nd = self.dec.next_needed();
3445 self.nak_history = self.nak_history.split_off(&nd);
3446 }
3447 // Hold-time deadline: a gap held longer than max_hold is skipped
3448 // so the stream is not blocked forever by an unrecoverable block.
3449 let head = self.dec.next_needed();
3450 if head != self.head_block {
3451 self.head_block = head;
3452 self.head_since = Instant::now();
3453 } else if self.head_since.elapsed() > self.max_hold && self.dec.window_len() > 0 {
3454 out.extend(self.dec.skip_head());
3455 self.head_block = self.dec.next_needed();
3456 self.head_since = Instant::now();
3457 }
3458 Ok(out)
3459 }
3460
3461 /// Encode and dispatch a feedback packet to `peer`. With no feedback
3462 /// delay configured it sends inline; with a delay it queues for release
3463 /// by [`flush_delayed_feedback`](Self::flush_delayed_feedback), so a
3464 /// loopback run can reproduce a real link's recovery round-trip.
3465 /// Feedback is best-effort and self-healing (the ack frontier is
3466 /// cumulative), so a transient send error must not abort the loop.
3467 /// Encode the receiver-side control state as a CONTROL packet: an ACK
3468 /// frame, a NAK frame when one is pending, a LOSS frame with the fused
3469 /// channel readings, and a PATH frame echoing the peer's last observed
3470 /// TTL / ECN so the sender's controller sees hop-count shifts and ECN
3471 /// congestion before they reach the loss estimate.
3472 fn control_bytes(&self, fb: &Feedback) -> Vec<u8> {
3473 let mut cp = ControlPacket::new();
3474 // Which session this feedback describes. A sender that has just
3475 // restarted must not apply an ack frontier belonging to the
3476 // session it replaced: that frontier is far ahead of its own
3477 // block ids, so it would prune every block it still holds as
3478 // delivered and be left with nothing to resend.
3479 cp.session_announce = self.dec.session_epoch();
3480 cp.ack = Some(AckFrame {
3481 ack_through: fb.ack_through,
3482 });
3483 if fb.nak_block != NAK_NONE {
3484 cp.nak = Some(NakFrame {
3485 block: fb.nak_block,
3486 mask: fb.nak_mask,
3487 });
3488 }
3489 cp.loss = Some(LossFrame {
3490 loss_x255: fb.loss_x255,
3491 burstiness_x255: fb.burstiness_x255,
3492 owd_trend_class: fb.owd_trend_class,
3493 loss_class: fb.loss_class,
3494 });
3495 if self.last_ttl != 0 {
3496 cp.path = Some(PathFrame {
3497 ttl: self.last_ttl,
3498 ecn: self.last_tos & 0b11,
3499 hop_count: crate::path_sensor::hop_count_from_ttl(self.last_ttl),
3500 ce_count: self.ce_count,
3501 ect_count: self.ect_count,
3502 });
3503 }
3504 // Our egress path MTU, so a handoff on this (receiver) end rides the
3505 // feedback to the sender's controller. The observer watches this host's
3506 // routes rather than any one peer, so the receiver samples it and the
3507 // session echoes what it was given.
3508 if self.local_pmtu != 0 {
3509 cp.pmtu = Some(PmtuFrame { pmtu: self.local_pmtu });
3510 }
3511 // Bidirectional loss accounting: report how many feedback packets we
3512 // have sent and how many sender heartbeats we have received, so the
3513 // sender separates forward (data) loss from reverse (feedback) loss.
3514 cp.loss_acct = Some(LossAcctFrame {
3515 seq: self.ctrl_out,
3516 last_recv_seq: self.ctrl_recv,
3517 });
3518 // WBest report (item 13): our measured available bandwidth / effective
3519 // capacity, so the sender can cross-check its passive BtlBw.
3520 if self.wbest_capacity_kbps != 0 {
3521 cp.avail_bw = Some(crate::control_frame::AvailBwFrame {
3522 avail_kbps: self.wbest_avail_kbps,
3523 capacity_kbps: self.wbest_capacity_kbps,
3524 });
3525 }
3526 // Sprout forecast (item 16): the 5th-percentile next-tick deliverable
3527 // rate, so the sender pre-sizes ahead of a dip.
3528 let fc_kbps = (self.forecast.forecast_bps() * 8.0 / 1000.0) as u64;
3529 if fc_kbps != 0 {
3530 cp.forecast = Some(crate::control_frame::ForecastFrame {
3531 forecast_kbps: fc_kbps,
3532 });
3533 }
3534 // LEO cadence (item 17): a detected handover period and time-to-next-spike
3535 // (deciseconds), so the sender pre-arms one cycle ahead.
3536 if let Some((period_s, conf)) = self.periodicity.detected_period() {
3537 let to_spike = self.periodicity.secs_to_next_spike().unwrap_or(0.0);
3538 cp.periodicity = Some(crate::control_frame::PeriodicityFrame {
3539 period_ds: (period_s * 10.0).round() as u64,
3540 secs_to_spike_ds: (to_spike * 10.0).round() as u64,
3541 confidence_x255: (conf.clamp(0.0, 1.0) * 255.0) as u8,
3542 });
3543 }
3544 encode_control(&cp)
3545 }
3546
3547 fn queue_feedback(&mut self, peer: SocketAddr, fb: &Feedback) {
3548 // Item 16: integrate one forecast tick before building the feedback that
3549 // carries the forecast.
3550 self.maybe_observe_forecast();
3551 // Count this feedback packet as sent BEFORE building it, so the LossAcct
3552 // seq it carries includes itself.
3553 self.ctrl_out = self.ctrl_out.wrapping_add(1);
3554 let fbuf = self.control_bytes(fb);
3555 // Inject reverse-path loss: the receiver did send it (ctrl_out counted
3556 // it), but it never reaches the sender, so the sender's peer_acked lags.
3557 if self.fb_drop_pct > 0 {
3558 self.fb_drop_rng = self
3559 .fb_drop_rng
3560 .wrapping_mul(6364136223846793005)
3561 .wrapping_add(1442695040888963407);
3562 if ((self.fb_drop_rng >> 33) as u32) % 100 < self.fb_drop_pct {
3563 return;
3564 }
3565 }
3566 if self.fb_delay.is_zero() {
3567 self.send_feedback_bytes(&fbuf, peer);
3568 } else {
3569 self.fb_pending
3570 .push_back((Instant::now() + self.fb_delay, fbuf));
3571 }
3572 }
3573
3574 /// Send one feedback datagram to the peer. The receive socket is
3575 /// connected on Linux/FreeBSD (for recvmmsg / GRO), and BSD rejects
3576 /// `send_to` on a connected UDP socket with EISCONN - so `send()` once
3577 /// connected, `send_to()` only while still unconnected (Windows / other).
3578 fn send_feedback_bytes(&self, bytes: &[u8], peer: SocketAddr) {
3579 if self.connected {
3580 // A connected send carries the socket's latched error: after a
3581 // peer dies, the ICMP unreachable it provoked surfaces here as
3582 // ConnectionReset and every later send fails the same way.
3583 // Swallowing that loses the feedback silently, so fall back to
3584 // an addressed send, which is unaffected.
3585 if self.sock.send(bytes).is_ok() {
3586 return;
3587 }
3588 }
3589 self.sock.send_to(bytes, peer).ok();
3590 }
3591
3592 /// Send any delayed feedback whose release time has arrived. A no-op
3593 /// when no feedback delay is configured. The queue is in release-time
3594 /// order (pushes use a monotonic clock), so a front-to-back drain
3595 /// stops at the first not-yet-due entry.
3596 fn flush_delayed_feedback(&mut self) {
3597 if self.fb_pending.is_empty() {
3598 return;
3599 }
3600 let now = Instant::now();
3601 let Some(peer) = self.peer else { return };
3602 while let Some((due, _)) = self.fb_pending.front() {
3603 if *due > now {
3604 break;
3605 }
3606 let (_, bytes) = self.fb_pending.pop_front().unwrap();
3607 self.send_feedback_bytes(&bytes, peer);
3608 }
3609 }
3610
3611 /// Send one feedback packet to the peer with tail-ARQ drive (used as
3612 /// a grace flush after all items are delivered, so the sender learns
3613 /// the final ack).
3614 pub fn nudge_feedback(&mut self) -> io::Result<()> {
3615 if let Some(peer) = self.peer {
3616 self.ctrl_out = self.ctrl_out.wrapping_add(1);
3617 let fb = self.dec.feedback(true);
3618 let fbuf = self.control_bytes(&fb);
3619 self.send_feedback_bytes(&fbuf, peer);
3620 }
3621 Ok(())
3622 }
3623}
3624
3625impl ReliableUdpReceiver {
3626 /// Bind `local`. The socket gets a short read timeout so the receiver parks
3627 /// on data yet wakes often enough to drive tail-ARQ feedback. No session
3628 /// exists until a peer is seen; each session epoch that arrives opens one.
3629 pub fn bind(local: impl ToSocketAddrs) -> io::Result<Self> {
3630 let sock = UdpSocket::bind(local)?;
3631 sock.set_read_timeout(Some(Duration::from_millis(4)))?;
3632 size_socket_buffers(&sock);
3633 // Observe each datagram's TTL / ECN passively: request the cmsgs here
3634 // (Linux / FreeBSD via IP_RECVTTL / IP_RECVTOS, Windows via
3635 // IP_HOPLIMIT / IP_RECVTOS / IP_ECN) and read them on the recv path.
3636 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
3637 enable_ttl_ecn(&sock);
3638 #[cfg(target_os = "windows")]
3639 enable_ttl_ecn_win(&sock);
3640 // Wrap as the plain-UDP DgramSock backend after the raw-fd cmsg setup;
3641 // the standalone path keeps the fd (via as_udp) for the TTL/ECN recvmsg.
3642 let sock = crate::dgram::DgramSock::from_udp(sock);
3643 Ok(Self {
3644 sock: std::sync::Arc::new(sock),
3645 sessions: HashMap::new(),
3646 order: Vec::new(),
3647 pending_admissions: HashMap::new(),
3648 session_ceiling: None,
3649 session_refusals: 0,
3650 session_nonce_seq: 0,
3651 session_changed: false,
3652 session_admissions: 0,
3653 session_admission_failures: 0,
3654 start: Instant::now(),
3655 net_events: NetEventObserver::start(None),
3656 multi_peer: false,
3657 net_event_shift_peak: 0.0,
3658 cfg: RsSessionConfig {
3659 // Default: hold a gap for a long time so delivery is
3660 // effectively reliable; recovery almost always lands first.
3661 max_hold: Duration::from_secs(60),
3662 fb_delay: Duration::ZERO,
3663 nak_batch: MAX_NAKS_PER_CYCLE,
3664 debug_drop_pct: 0,
3665 drop_rng: 0x9E3779B97F4A7C15,
3666 ge_loss_p: 0,
3667 ge_loss_r: 0,
3668 drop_block_mod: 0,
3669 burst_at: 0,
3670 burst_len: 0,
3671 fb_drop_pct: 0,
3672 fb_drop_rng: 0x243F6A8885A308D3,
3673 },
3674 })
3675 }
3676
3677 /// Open a window for `epoch`, or `None` when the receiver will not carry
3678 /// another peer.
3679 fn open_session(&mut self, epoch: u32) -> Option<&mut RsSession> {
3680 if !self.sessions.contains_key(&epoch) {
3681 if self.session_ceiling.is_some_and(|max| self.sessions.len() >= max) {
3682 self.session_refusals += 1;
3683 return None;
3684 }
3685 // Past the first session the socket must accept every address.
3686 if !self.sessions.is_empty() {
3687 self.dissolve_peer_association();
3688 }
3689 let s = RsSession::new(std::sync::Arc::clone(&self.sock), self.cfg);
3690 self.sessions.insert(epoch, s);
3691 self.order.push(epoch);
3692 }
3693 self.sessions.get_mut(&epoch)
3694 }
3695
3696 /// Drop the socket's single-peer association and the fast paths that read
3697 /// through it. Connecting to an unspecified address is how "no peer" is
3698 /// expressed through the portable API.
3699 fn dissolve_peer_association(&mut self) {
3700 if self.sock.connect(UNSPECIFIED_PEER).is_ok() {
3701 for s in self.sessions.values_mut() {
3702 s.connected = false;
3703 #[cfg(target_os = "linux")]
3704 {
3705 s.gro_on = false;
3706 }
3707 }
3708 }
3709 }
3710
3711 /// Whether the one live session still holds the socket association, or has
3712 /// yet to bind one.
3713 fn solo_connected(&self) -> bool {
3714 match self.order.first().and_then(|e| self.sessions.get(e)) {
3715 Some(s) => s.connected || s.peer.is_none(),
3716 None => true,
3717 }
3718 }
3719
3720 /// Give up the socket association once its peer has been silent past
3721 /// [`PEER_SILENCE_TIMEOUT`], so a peer arriving on a fresh address is heard.
3722 fn release_silent_peer(&mut self) {
3723 if self.multi_peer || self.sessions.len() != 1 {
3724 return;
3725 }
3726 let stale = self
3727 .order
3728 .first()
3729 .and_then(|e| self.sessions.get(e))
3730 .is_some_and(|s| s.connected && s.last_data_at.elapsed() > PEER_SILENCE_TIMEOUT);
3731 if stale {
3732 self.dissolve_peer_association();
3733 }
3734 }
3735
3736 /// Receive whatever has arrived and deliver in-order items, each tagged
3737 /// with the session epoch of the peer that sent it.
3738 ///
3739 /// Items are ordered within an epoch and unordered across epochs.
3740 pub fn poll_from(&mut self) -> io::Result<Vec<(u32, Vec<u8>)>> {
3741 let mut tagged: Vec<(u32, Vec<u8>)> = Vec::new();
3742 let shift = self.net_events.path_shift();
3743 if shift > self.net_event_shift_peak {
3744 self.net_event_shift_peak = shift;
3745 }
3746 let pmtu = self.net_events.pmtu().unwrap_or(0);
3747
3748 // A connected socket hears one address, so a peer that has gone quiet
3749 // past PEER_SILENCE_TIMEOUT gives up the association and the receiver
3750 // reads unconnected until the next session binds one.
3751 self.release_silent_peer();
3752
3753 // One peer: the connected fast path. Several: per-datagram reads with
3754 // source capture, routed by epoch.
3755 let timed_out = if !self.multi_peer && self.sessions.len() == 1 && self.solo_connected() {
3756 let mut items = Vec::new();
3757 let epoch = self.order[0];
3758 let t = {
3759 let s = self.sessions.get_mut(&epoch).expect("len == 1");
3760 s.local_pmtu = pmtu;
3761 s.recv_into(&mut items)?
3762 };
3763 tagged.extend(items.into_iter().map(|i| (epoch, i)));
3764 t
3765 } else {
3766 self.drain_unconnected(&mut tagged, pmtu)?
3767 };
3768
3769 self.expire_stale_admissions();
3770 self.send_admission_challenges()?;
3771
3772 let ids = self.order.clone();
3773 // A session's service error is a send toward its own peer and stays
3774 // with that session. Every session is serviced each tick, and `tagged`
3775 // keeps this tick's items.
3776 for epoch in ids {
3777 if let Some(mut s) = self.sessions.remove(&epoch) {
3778 s.local_pmtu = pmtu;
3779 let r = s.service(timed_out);
3780 self.sessions.insert(epoch, s);
3781 if let Ok(items) = r {
3782 tagged.extend(items.into_iter().map(|i| (epoch, i)));
3783 }
3784 }
3785 }
3786 Ok(tagged)
3787 }
3788
3789 /// Receive one datagram, decode it, send feedback, and return any items
3790 /// that became deliverable in stream order. Peer attribution is dropped;
3791 /// use [`poll_from`](Self::poll_from) when several peers are live.
3792 pub fn poll(&mut self) -> io::Result<Vec<Vec<u8>>> {
3793 Ok(self.poll_from()?.into_iter().map(|(_, item)| item).collect())
3794 }
3795
3796 /// The unconnected multi-peer receive path: one datagram at a time, with
3797 /// the source captured, routed by the epoch the datagram carries.
3798 fn drain_unconnected(
3799 &mut self,
3800 tagged: &mut Vec<(u32, Vec<u8>)>,
3801 pmtu: u16,
3802 ) -> io::Result<bool> {
3803 let mut buf = [0u8; RECV_BUF];
3804 match self.sock.recv_from(&mut buf) {
3805 Ok((n, src)) => {
3806 self.route_datagram(&buf[..n], src, tagged, pmtu);
3807 Ok(false)
3808 }
3809 Err(e)
3810 if matches!(
3811 e.kind(),
3812 io::ErrorKind::WouldBlock
3813 | io::ErrorKind::TimedOut
3814 | io::ErrorKind::ConnectionReset
3815 ) =>
3816 {
3817 Ok(true)
3818 }
3819 Err(e) => Err(e),
3820 }
3821 }
3822
3823 /// Route one datagram to the window that owns its session epoch. The first
3824 /// epoch seen opens a window directly; every epoch after it is challenged
3825 /// first.
3826 fn route_datagram(
3827 &mut self,
3828 buf: &[u8],
3829 src: SocketAddr,
3830 tagged: &mut Vec<(u32, Vec<u8>)>,
3831 pmtu: u16,
3832 ) {
3833 if self.try_admit(buf, src) {
3834 return;
3835 }
3836 let epoch = match datagram_epoch(buf) {
3837 Some(e) => e,
3838 // No epoch in the datagram (a control packet): it belongs to the
3839 // session bound to this address, else the one that spoke last.
3840 None => {
3841 let by_addr = self
3842 .order
3843 .iter()
3844 .find(|e| self.sessions.get(e).and_then(|s| s.peer) == Some(src))
3845 .copied();
3846 match by_addr.or_else(|| self.order.last().copied()) {
3847 Some(e) => e,
3848 None => return,
3849 }
3850 }
3851 };
3852 if !self.sessions.contains_key(&epoch) && !self.sessions.is_empty() {
3853 self.begin_admission(epoch, src);
3854 return;
3855 }
3856 let mut items = Vec::new();
3857 if let Some(s) = self.open_session(epoch) {
3858 s.local_pmtu = pmtu;
3859 if s.peer != Some(src) {
3860 s.peer = Some(src);
3861 }
3862 s.process_datagram(buf, &mut items);
3863 }
3864 tagged.extend(items.into_iter().map(|i| (epoch, i)));
3865 }
3866
3867 /// Arm a challenge for an epoch asking to be admitted.
3868 fn begin_admission(&mut self, epoch: u32, addr: SocketAddr) {
3869 if self.session_ceiling.is_some_and(|max| self.pending_admissions.len() >= max) {
3870 self.session_refusals += 1;
3871 return;
3872 }
3873 if let Some((a, _, _)) = self.pending_admissions.get(&epoch)
3874 && *a == addr
3875 {
3876 return;
3877 }
3878 self.session_nonce_seq = self.session_nonce_seq.wrapping_add(1);
3879 let entropy = self.start.elapsed().as_nanos() as u64;
3880 let mut x = entropy ^ self.session_nonce_seq.rotate_left(32) ^ u64::from(epoch);
3881 x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
3882 x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
3883 // Masked to what a varint carries without clamping, or the echo would
3884 // come back a different number than was stored.
3885 let nonce = (x ^ (x >> 31)) & crate::control_frame::NONCE_MASK;
3886 self.pending_admissions.insert(epoch, (addr, nonce, Instant::now()));
3887 }
3888
3889 /// (Re)send every outstanding admission challenge.
3890 fn send_admission_challenges(&mut self) -> io::Result<()> {
3891 let pending: Vec<(u32, SocketAddr, u64)> = self
3892 .pending_admissions
3893 .iter()
3894 .map(|(e, (a, n, _))| (*e, *a, *n))
3895 .collect();
3896 for (epoch, addr, nonce) in pending {
3897 let mut cp = ControlPacket::new();
3898 cp.session_challenge = Some(crate::control_frame::SessionFrame { epoch, nonce });
3899 let wire = encode_control(&cp);
3900 self.sock.send_to(&wire, addr)?;
3901 }
3902 Ok(())
3903 }
3904
3905 /// Retire challenges unanswered past [`SESSION_CHALLENGE_TIMEOUT`],
3906 /// counting each into `session_admission_failures`.
3907 fn expire_stale_admissions(&mut self) {
3908 let before = self.pending_admissions.len();
3909 self.pending_admissions
3910 .retain(|_, (_, _, sent)| sent.elapsed() <= SESSION_CHALLENGE_TIMEOUT);
3911 self.session_admission_failures += (before - self.pending_admissions.len()) as u64;
3912 }
3913
3914 /// Open a window for an epoch whose challenge nonce came back from the
3915 /// address it was sent to. Returns whether `buf` was such an answer.
3916 fn try_admit(&mut self, buf: &[u8], src: SocketAddr) -> bool {
3917 if !is_control(buf) {
3918 return false;
3919 }
3920 let Some(cp) = decode_control(buf) else { return false };
3921 let Some(sr) = cp.session_response else { return false };
3922 let Some((addr, nonce, _)) = self.pending_admissions.get(&sr.epoch).copied() else {
3923 return false;
3924 };
3925 if addr != src || nonce != sr.nonce {
3926 return false;
3927 }
3928 self.pending_admissions.remove(&sr.epoch);
3929 if let Some(s) = self.open_session(sr.epoch) {
3930 s.peer = Some(src);
3931 s.dec.adopt_epoch(sr.epoch);
3932 s.nak_history.clear();
3933 s.last_data_at = Instant::now();
3934 self.session_admissions += 1;
3935 self.session_changed = true;
3936 return true;
3937 }
3938 false
3939 }
3940
3941 /// Serve several peers concurrently. The socket stays unconnected and each
3942 /// datagram is read singly, its source captured, and routed by the session
3943 /// epoch it carries. Gives up the GRO / `recvmmsg` / `WSARecvMsg` fast
3944 /// paths, which read an address-associated socket, so throughput is below
3945 /// the point-to-point figures.
3946 pub fn with_multi_peer(mut self) -> Self {
3947 self.multi_peer = true;
3948 self
3949 }
3950
3951 /// Swap the datagram socket for one the caller already built (a demux
3952 /// socket the unified endpoint shares across both codes). Live sessions
3953 /// pick it up, since they hold the same handle.
3954 pub fn set_sock(&mut self, sock: crate::dgram::DgramSock) {
3955 // A demux socket is shared with the other code and fed by a reader that
3956 // takes every source address, so there is no peer association to keep
3957 // and the receiver must route by epoch. The connected fast paths do not
3958 // apply to it either way.
3959 if sock.backend() == crate::dgram::DgramBackend::Demux {
3960 self.multi_peer = true;
3961 }
3962 let sock = std::sync::Arc::new(sock);
3963 self.sock = std::sync::Arc::clone(&sock);
3964 for s in self.sessions.values_mut() {
3965 s.sock = std::sync::Arc::clone(&sock);
3966 s.connected = false;
3967 }
3968 }
3969
3970 /// The epoch of the most recently opened session, or `None` before any peer
3971 /// is seen. Ambiguous once several peers are live - prefer
3972 /// [`live_sessions`](Self::live_sessions).
3973 pub fn session_epoch(&self) -> Option<u32> {
3974 self.order.last().copied()
3975 }
3976
3977 /// Send one feedback round to every live peer without waiting for a
3978 /// datagram.
3979 pub fn nudge_feedback(&mut self) -> io::Result<()> {
3980 // A session's send error stays with that session; every session gets
3981 // its feedback round.
3982 let ids = self.order.clone();
3983 for epoch in ids {
3984 if let Some(mut s) = self.sessions.remove(&epoch) {
3985 let r = s.nudge_feedback();
3986 self.sessions.insert(epoch, s);
3987 r.ok();
3988 }
3989 }
3990 Ok(())
3991 }
3992
3993 /// Drop `pct` percent of incoming data datagrams (seeded, reproducible) to
3994 /// exercise FEC / ARQ on a lossless link. Stamped onto each session as it
3995 /// opens, so every peer sees the same injected rate.
3996 pub fn with_debug_loss(mut self, pct: u32, seed: u64) -> Self {
3997 self.cfg.debug_drop_pct = pct.min(100);
3998 self.cfg.drop_rng = seed | 1;
3999 self
4000 }
4001
4002 /// Gilbert-Elliott burst-loss injection, per-10000 transition
4003 /// probabilities.
4004 pub fn with_gilbert_loss(mut self, p_per_10k: u32, r_per_10k: u32, seed: u64) -> Self {
4005 self.cfg.ge_loss_p = p_per_10k;
4006 self.cfg.ge_loss_r = r_per_10k.max(1);
4007 self.cfg.drop_rng = seed | 1;
4008 self
4009 }
4010
4011 /// How long a gap is held while FEC and ARQ recover it before the stream is
4012 /// advanced past it.
4013 pub fn with_max_hold(mut self, hold: Duration) -> Self {
4014 self.cfg.max_hold = hold;
4015 self
4016 }
4017
4018 /// Inject a one-way feedback delay, to model a link's return latency.
4019 pub fn with_feedback_delay(mut self, delay: Duration) -> Self {
4020 self.cfg.fb_delay = delay;
4021 self
4022 }
4023
4024 /// Cap how many gaps one selective-NAK cycle re-requests.
4025 pub fn with_nak_batch(mut self, batch: usize) -> Self {
4026 self.cfg.nak_batch = batch.max(1);
4027 self
4028 }
4029
4030 /// Drop `pct` percent of outbound feedback datagrams (diagnostics).
4031 pub fn with_feedback_drop(mut self, pct: u32) -> Self {
4032 self.cfg.fb_drop_pct = pct.min(100);
4033 self
4034 }
4035
4036 /// Drop every shard of any data block whose id is a multiple of `m`.
4037 pub fn with_block_drop_mod(mut self, m: u32) -> Self {
4038 self.cfg.drop_block_mod = m;
4039 self
4040 }
4041
4042 /// Drop every data datagram arriving in `[at, at + len)` by arrival index.
4043 pub fn with_burst_loss(mut self, at: u64, len: u64) -> Self {
4044 self.cfg.burst_at = at;
4045 self.cfg.burst_len = len;
4046 self
4047 }
4048
4049 /// The bound local address (useful when binding to port 0).
4050 pub fn local_addr(&self) -> io::Result<SocketAddr> {
4051 self.sock.local_addr()
4052 }
4053
4054 /// Count of OS path events this end's active observer has seen.
4055 pub fn net_event_count(&self) -> u64 {
4056 self.net_events.event_count()
4057 }
4058
4059 /// This endpoint's egress path MTU in bytes (0 = unknown).
4060 pub fn local_pmtu(&self) -> u16 {
4061 self.net_events.pmtu().unwrap_or(0)
4062 }
4063
4064 /// The observer's current decaying path-shift (telemetry).
4065 pub fn net_event_shift(&self) -> f32 {
4066 self.net_events.path_shift()
4067 }
4068
4069 /// The peak path shift reached over the run (telemetry).
4070 pub fn net_event_shift_peak(&self) -> f32 {
4071 self.net_event_shift_peak
4072 }
4073
4074 /// Synthetically fire a path event on this end (demo path).
4075 pub fn inject_path_event(&self) {
4076 self.net_events.inject_event();
4077 }
4078
4079 /// Synthetically set this endpoint's egress MTU (demo path).
4080 pub fn inject_pmtu(&self, mtu: u16) {
4081 self.net_events.inject_pmtu(mtu);
4082 }
4083
4084 /// Datagrams read off the socket, summed over peers.
4085 pub fn recv_count(&self) -> u64 {
4086 self.sessions.values().map(|s| s.recv_count()).sum()
4087 }
4088
4089 /// The path MTU last reported by the most recently opened peer (0 = none
4090 /// yet). Per-peer by nature; use [`peer_pmtu_of`](Self::peer_pmtu_of) when
4091 /// several are live.
4092 pub fn peer_pmtu(&self) -> u16 {
4093 self.order.last().and_then(|e| self.sessions.get(e)).map(|s| s.peer_pmtu()).unwrap_or(0)
4094 }
4095
4096 /// The path MTU reported by one peer.
4097 pub fn peer_pmtu_of(&self, epoch: u32) -> Option<u16> {
4098 self.sessions.get(&epoch).map(|s| s.peer_pmtu())
4099 }
4100
4101 /// The session epochs with a live decode window, in first-seen order.
4102 pub fn live_sessions(&self) -> Vec<u32> {
4103 self.order.clone()
4104 }
4105
4106 /// Bound the live windows and the candidates under challenge at `max`.
4107 /// Unbounded unless set. A peer turned away by the ceiling is counted in
4108 /// [`session_refusals`](Self::session_refusals) rather than dropped
4109 /// silently.
4110 pub fn with_session_ceiling(mut self, max: usize) -> Self {
4111 self.session_ceiling = Some(max.max(1));
4112 self
4113 }
4114
4115 /// Peers refused a decode window by a declared ceiling. Non-zero means a
4116 /// peer that reached this receiver was not served.
4117 pub fn session_refusals(&self) -> u64 {
4118 self.session_refusals
4119 }
4120
4121 /// The most recently opened session, which the per-peer telemetry below
4122 /// reports for.
4123 fn newest(&self) -> Option<&RsSession> {
4124 self.order.last().and_then(|e| self.sessions.get(e))
4125 }
4126
4127 /// Peak per-block loss seen, x255, over every peer.
4128 pub fn peak_loss_x255(&self) -> u8 {
4129 self.sessions.values().map(|s| s.peak_loss_x255()).max().unwrap_or(0)
4130 }
4131
4132 /// Blocks the decoder reconstructed that later proved already complete,
4133 /// summed over peers.
4134 pub fn false_recovery_count(&self) -> u64 {
4135 self.sessions.values().map(|s| s.false_recovery_count()).sum()
4136 }
4137
4138 /// Drive the Gilbert-Elliott injector's bad state on every live session.
4139 pub fn set_ge_burst(&mut self, on: bool) {
4140 for s in self.sessions.values_mut() {
4141 s.set_ge_burst(on);
4142 }
4143 }
4144
4145 /// Set the injected data-loss percentage on every live session.
4146 pub fn set_debug_loss(&mut self, pct: u32) {
4147 self.cfg.debug_drop_pct = pct.min(100);
4148 for s in self.sessions.values_mut() {
4149 s.set_debug_loss(pct);
4150 }
4151 }
4152
4153 /// Mean burst length of the newest peer's fitted loss model.
4154 pub fn mean_burst_len(&self) -> f32 {
4155 self.newest().map(|s| s.mean_burst_len()).unwrap_or(0.0)
4156 }
4157
4158 /// One-way-delay skew of the newest peer's path.
4159 pub fn owd_skew(&self) -> f64 {
4160 self.newest().map(|s| s.owd_skew()).unwrap_or(0.0)
4161 }
4162
4163 /// Debiased one-way-delay trend of the newest peer's path.
4164 pub fn owd_trend_debiased(&self) -> f64 {
4165 self.newest().map(|s| s.owd_trend_debiased()).unwrap_or(0.0)
4166 }
4167
4168 /// The newest peer's current ACK cadence.
4169 pub fn ack_interval(&self) -> Duration {
4170 self.newest().map(|s| s.ack_interval()).unwrap_or(ACK_INTERVAL)
4171 }
4172
4173 /// Reverse-path (feedback) loss fraction toward the newest peer.
4174 pub fn feedback_loss_est(&self) -> f32 {
4175 self.newest().map(|s| s.feedback_loss_est()).unwrap_or(0.0)
4176 }
4177
4178 /// The newest peer's reported `(link_class, link_quality)`.
4179 pub fn peer_link(&self) -> (u8, u8) {
4180 self.newest().map(|s| s.peer_link()).unwrap_or((0, 0))
4181 }
4182
4183 /// AccECN `(ce_count, ect_count)` observed from the newest peer.
4184 pub fn accecn_counts(&self) -> (u64, u64) {
4185 self.newest().map(|s| s.accecn_counts()).unwrap_or((0, 0))
4186 }
4187
4188 /// Arrival-rate forecast for the newest peer, bits per second.
4189 pub fn forecast_bps(&self) -> u64 {
4190 self.newest().map(|s| s.forecast_bps()).unwrap_or(0)
4191 }
4192
4193 /// LEO handover cadence detected on the newest peer's path.
4194 pub fn leo_cadence(&self) -> Option<(f64, f64, f64)> {
4195 self.newest().and_then(|s| s.leo_cadence())
4196 }
4197
4198 /// WBest `(available, capacity)` estimate for the newest peer, bits/s.
4199 pub fn wbest_bps(&self) -> (u64, u64) {
4200 self.newest().map(|s| s.wbest_bps()).unwrap_or((0, 0))
4201 }
4202
4203 /// The block blocking in-order delivery on the newest peer:
4204 /// `(block_id, received_shards, k, decoded)`.
4205 pub fn head_status(&self) -> Option<(u32, u32, usize, bool)> {
4206 self.newest().and_then(|s| s.head_status())
4207 }
4208
4209 /// Whether a window was admitted since the last call. Edge-triggered.
4210 pub fn take_session_changed(&mut self) -> bool {
4211 std::mem::replace(&mut self.session_changed, false)
4212 }
4213
4214 /// `(admitted, challenges that went unanswered)`. The second rising
4215 /// without the first is what a forged epoch looks like from here.
4216 pub fn session_adoption_counts(&self) -> (u64, u64) {
4217 (self.session_admissions, self.session_admission_failures)
4218 }
4219}
4220
4221#[cfg(test)]
4222mod tests {
4223 use super::*;
4224 use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
4225 use std::sync::mpsc;
4226 use std::sync::Arc;
4227
4228 /// k must fit the u32 shard bitmap: a k > MAX_SHARDS would overflow
4229 /// `1 << shard_index` and silently corrupt delivery, so bind rejects it.
4230 #[test]
4231 fn bind_rejects_oversized_k() {
4232 let peer: SocketAddr = "127.0.0.1:9".parse().unwrap();
4233 assert!(
4234 ReliableUdpSender::bind("127.0.0.1:0", peer, 33, 1, 64).is_err(),
4235 "k=33 > MAX_SHARDS must be rejected"
4236 );
4237 assert!(
4238 ReliableUdpSender::bind("127.0.0.1:0", peer, 0, 1, 64).is_err(),
4239 "k=0 must be rejected"
4240 );
4241 assert!(
4242 ReliableUdpSender::bind("127.0.0.1:0", peer, 16, 8, 64).is_ok(),
4243 "k=16 r=8 (k+r=24) must be accepted"
4244 );
4245 }
4246
4247 /// Real loopback sockets, real UDP datagrams, diagnostic loss on the
4248 /// receiver. Ships `n` u64 items and asserts exact in-order
4249 /// delivery, proving the FEC + ARQ stack over an actual socket.
4250 fn loopback_round_trip(n: u64, k: usize, r: usize, loss_pct: u32, seed: u64) {
4251 let (addr_tx, addr_rx) = mpsc::channel();
4252 let (done_tx, done_rx) = mpsc::channel();
4253
4254 let rx = std::thread::spawn(move || {
4255 let mut recv = ReliableUdpReceiver::bind("127.0.0.1:0")
4256 .unwrap()
4257 .with_debug_loss(loss_pct, seed);
4258 addr_tx.send(recv.local_addr().unwrap()).unwrap();
4259 let mut got: Vec<u64> = Vec::new();
4260 let start = Instant::now();
4261 while (got.len() as u64) < n {
4262 if start.elapsed() > Duration::from_secs(20) {
4263 break;
4264 }
4265 for item in recv.poll().unwrap() {
4266 got.push(u64::from_le_bytes(item.try_into().unwrap()));
4267 }
4268 }
4269 // Grace: let the sender learn the final ack.
4270 for _ in 0..10 {
4271 recv.nudge_feedback().ok();
4272 std::thread::sleep(Duration::from_millis(2));
4273 }
4274 done_tx.send(()).ok();
4275 got
4276 });
4277
4278 let recv_addr = addr_rx.recv().unwrap();
4279 let tx = std::thread::spawn(move || {
4280 let mut send =
4281 ReliableUdpSender::bind("127.0.0.1:0", recv_addr, k, r, 8).unwrap();
4282 for i in 0..n {
4283 while send.flow_blocked() {
4284 send.drain_until_acked(Duration::from_millis(50)).ok();
4285 }
4286 send.send_item(&i.to_le_bytes()).unwrap();
4287 }
4288 send.flush().unwrap();
4289 send.drain_until_acked(Duration::from_secs(15)).unwrap();
4290 done_rx.recv_timeout(Duration::from_secs(20)).ok();
4291 });
4292
4293 let got = rx.join().unwrap();
4294 tx.join().unwrap();
4295 let expected: Vec<u64> = (0..n).collect();
4296 assert_eq!(got, expected, "loopback exact in-order delivery");
4297 }
4298
4299 #[test]
4300 fn loopback_clean() {
4301 loopback_round_trip(500, 8, 2, 0, 1);
4302 }
4303
4304 #[test]
4305 fn loopback_lossy_fec() {
4306 // ~12% injected loss, r=3 over k=8: FEC carries most blocks.
4307 loopback_round_trip(500, 8, 3, 12, 7);
4308 }
4309
4310 #[test]
4311 fn loopback_heavy_arq() {
4312 // ~30% injected loss: ARQ fallback must carry the remainder.
4313 loopback_round_trip(300, 8, 2, 30, 1234);
4314 }
4315
4316 /// Three concurrent senders, which is the smallest number that forces two
4317 /// separate admission challenges. With two peers one always takes the
4318 /// free first-admission slot, so a broken challenge path still delivers
4319 /// both streams and a two-peer test passes.
4320 #[test]
4321 fn three_concurrent_rs_senders_all_deliver() {
4322 const PER: u64 = 120;
4323 const SENDERS: u64 = 3;
4324 let mut recv = ReliableUdpReceiver::bind("127.0.0.1:0").unwrap().with_multi_peer();
4325 let addr = recv.local_addr().unwrap();
4326
4327 let gate = Arc::new(std::sync::Barrier::new(SENDERS as usize));
4328 let done = Arc::new(AtomicBool::new(false));
4329 let mut txs = Vec::new();
4330 for s in 0..SENDERS {
4331 let stop = Arc::clone(&done);
4332 let gate = Arc::clone(&gate);
4333 txs.push(std::thread::spawn(move || {
4334 let mut send = ReliableUdpSender::bind("127.0.0.1:0", addr, 4, 2, 8).unwrap();
4335 gate.wait();
4336 for i in 0..PER {
4337 send.send_item(&((s << 56) | i).to_le_bytes()).unwrap();
4338 }
4339 send.flush().unwrap();
4340 while !stop.load(AtomicOrdering::Relaxed) {
4341 send.drain_until_acked(Duration::from_millis(50)).ok();
4342 }
4343 }));
4344 }
4345
4346 let mut got: Vec<u64> = Vec::new();
4347 let start = Instant::now();
4348 while (got.len() as u64) < PER * SENDERS && start.elapsed() < Duration::from_secs(30) {
4349 for item in recv.poll().unwrap() {
4350 got.push(u64::from_le_bytes(item.try_into().unwrap()));
4351 }
4352 }
4353 done.store(true, AtomicOrdering::Relaxed);
4354 for t in txs {
4355 t.join().ok();
4356 }
4357
4358 let live = recv.live_sessions().len();
4359 let (admitted, unanswered) = recv.session_adoption_counts();
4360 for s in 0..SENDERS {
4361 let mine: Vec<u64> =
4362 got.iter().filter(|v| (*v >> 56) == s).map(|v| v & 0x00FF_FFFF_FFFF_FFFF).collect();
4363 assert_eq!(
4364 mine,
4365 (0..PER).collect::<Vec<_>>(),
4366 "sender {s} of {SENDERS} did not deliver ({live} windows live, \
4367 {admitted} admitted, {unanswered} challenges unanswered)",
4368 );
4369 }
4370 assert_eq!(
4371 live, SENDERS as usize,
4372 "expected a window per peer, got {live} ({admitted} admitted, \
4373 {unanswered} unanswered)",
4374 );
4375 }
4376
4377 /// Two independent block-RS senders, distinct session epochs, delivering
4378 /// to ONE receiver at the same time - the replication-mesh shape, where a
4379 /// node receives from several peers concurrently rather than from one peer
4380 /// that restarted.
4381 ///
4382 /// The RLC code carries this (each connection id decodes in its own
4383 /// window); block-RS holds a single session epoch, so the second sender's
4384 /// blocks are gated out by the epoch check ahead of the block-id checks.
4385 /// Each sender tags its items in the high byte so the streams stay
4386 /// distinguishable; ordering is asserted WITHIN a sender, since nothing
4387 /// orders one against the other.
4388 #[test]
4389 fn two_concurrent_rs_senders_both_deliver() {
4390 const PER: u64 = 200;
4391 const SENDERS: u64 = 2;
4392 let mut recv = ReliableUdpReceiver::bind("127.0.0.1:0").unwrap().with_multi_peer();
4393 let addr = recv.local_addr().unwrap();
4394
4395 // Both senders bind first and then start together. Without the barrier
4396 // a 40-item sender finishes before the other binds, so the receiver
4397 // sees a restart rather than a second live peer - the test passes
4398 // while never exercising concurrency at all.
4399 let gate = Arc::new(std::sync::Barrier::new(SENDERS as usize));
4400 let done = Arc::new(AtomicBool::new(false));
4401 let mut txs = Vec::new();
4402 let mut epochs = Vec::new();
4403 for s in 0..SENDERS {
4404 let stop = Arc::clone(&done);
4405 let gate = Arc::clone(&gate);
4406 let (etx, erx) = std::sync::mpsc::channel();
4407 txs.push(std::thread::spawn(move || {
4408 let mut send = ReliableUdpSender::bind("127.0.0.1:0", addr, 4, 2, 8).unwrap();
4409 etx.send(send.enc.epoch()).ok();
4410 gate.wait();
4411 for i in 0..PER {
4412 send.send_item(&((s << 56) | i).to_le_bytes()).unwrap();
4413 }
4414 send.flush().unwrap();
4415 while !stop.load(AtomicOrdering::Relaxed) {
4416 send.drain_until_acked(Duration::from_millis(50)).ok();
4417 }
4418 }));
4419 epochs.push(erx.recv_timeout(Duration::from_secs(5)).unwrap());
4420 }
4421 assert_ne!(epochs[0], epochs[1], "independent senders must draw distinct epochs");
4422
4423 let mut got: Vec<u64> = Vec::new();
4424 let start = Instant::now();
4425 while (got.len() as u64) < PER * SENDERS && start.elapsed() < Duration::from_secs(25) {
4426 for item in recv.poll().unwrap() {
4427 got.push(u64::from_le_bytes(item.try_into().unwrap()));
4428 }
4429 }
4430 done.store(true, AtomicOrdering::Relaxed);
4431 for t in txs {
4432 t.join().ok();
4433 }
4434
4435 let (adopted, unanswered) = recv.session_adoption_counts();
4436 for s in 0..SENDERS {
4437 let mine: Vec<u64> =
4438 got.iter().filter(|v| (*v >> 56) == s).map(|v| v & 0x00FF_FFFF_FFFF_FFFF).collect();
4439 assert_eq!(
4440 mine,
4441 (0..PER).collect::<Vec<_>>(),
4442 "sender {s} (epoch {}) must deliver every item in order alongside the other sender",
4443 epochs[s as usize],
4444 );
4445 }
4446 // Delivery alone does not prove the receiver carried two sessions. A
4447 // single-session receiver reaches the same result by ADOPTING back and
4448 // forth - each adoption resets the decoder and ARQ re-delivers - which
4449 // converges at this size and collapses at scale. Two live peers should
4450 // cost at most one adoption, so a count that tracks the traffic is the
4451 // thrash showing itself.
4452 assert!(
4453 adopted <= 1,
4454 "receiver thrashed between the two peers: {adopted} adoptions, {unanswered} \
4455 unanswered, for {SENDERS} concurrent senders",
4456 );
4457 }
4458
4459 /// A replacement sender is delivered once its epoch is challenged and
4460 /// answered. Both senders live in this process, so the second one's
4461 /// block ids start at zero against a frontier the first advanced -
4462 /// the state a restarted peer presents.
4463 ///
4464 /// The second sender is driven by `drain_until_acked`, which is what
4465 /// retransmits: `pump_feedback` only samples, beats and reads, so a
4466 /// sender that flushed once into a socket still bound to its dead
4467 /// predecessor would have nothing left to re-offer.
4468 #[test]
4469 fn restarted_sender_is_adopted_after_the_challenge() {
4470 const N: u64 = 40;
4471 let mut recv = ReliableUdpReceiver::bind("127.0.0.1:0").unwrap();
4472 let addr = recv.local_addr().unwrap();
4473
4474 let mut first = ReliableUdpSender::bind("127.0.0.1:0", addr, 4, 2, 8).unwrap();
4475 for i in 0..N {
4476 first.send_item(&i.to_le_bytes()).unwrap();
4477 }
4478 first.flush().unwrap();
4479
4480 let mut seen = 0u64;
4481 let start = Instant::now();
4482 while seen < N && start.elapsed() < Duration::from_secs(10) {
4483 seen += recv.poll().unwrap().len() as u64;
4484 first.pump_feedback().ok();
4485 }
4486 assert_eq!(seen, N, "first session did not deliver");
4487 let epoch_a = recv.session_epoch();
4488 assert!(epoch_a.is_some(), "no session epoch learned");
4489
4490 drop(first);
4491 let mut second = ReliableUdpSender::bind("127.0.0.1:0", addr, 4, 2, 8).unwrap();
4492 assert_ne!(
4493 second.enc.epoch(),
4494 recv.session_epoch().unwrap(),
4495 "the replacement drew the same epoch as its predecessor"
4496 );
4497 for i in 0..N {
4498 second.send_item(&(1000 + i).to_le_bytes()).unwrap();
4499 }
4500 second.flush().unwrap();
4501
4502 // The sender runs in its own thread, as every other loopback test
4503 // here does. Interleaving both halves in one thread makes each
4504 // side's progress depend on the other's blocking read, which is a
4505 // property of the test rather than of the transport.
4506 let done = Arc::new(AtomicBool::new(false));
4507 let stop = Arc::clone(&done);
4508 let tx = std::thread::spawn(move || {
4509 while !stop.load(AtomicOrdering::Relaxed) {
4510 second.drain_until_acked(Duration::from_millis(50)).ok();
4511 }
4512 });
4513
4514 let mut got = Vec::new();
4515 let start = Instant::now();
4516 while (got.len() as u64) < N && start.elapsed() < Duration::from_secs(25) {
4517 got.extend(recv.poll().unwrap());
4518 }
4519 done.store(true, AtomicOrdering::Relaxed);
4520 tx.join().ok();
4521 let (adopted, unanswered) = recv.session_adoption_counts();
4522 assert_eq!(
4523 got.len() as u64,
4524 N,
4525 "restarted sender delivered {}/{N} (adopted {adopted}, unanswered {unanswered})",
4526 got.len(),
4527 );
4528 assert_eq!(adopted, 1, "expected exactly one adoption");
4529 assert!(recv.take_session_changed(), "session_changed never raised");
4530 }
4531
4532 /// The item-12 active path-event slice over a real loopback bridge: an
4533 /// injected path event registers on the receiver, and each endpoint's
4534 /// egress MTU rides its `Pmtu` frame to the peer. The assertion is the
4535 /// cross-check `peer_pmtu == the other side's local_pmtu`, so it holds
4536 /// faithfully whether or not the host exposes a readable MTU (both 0 on a
4537 /// host without one). Distinct injected MTUs make the round-trip
4538 /// discriminating rather than coincidental.
4539 #[test]
4540 fn path_event_registers_and_pmtu_round_trips() {
4541 let n = 400u64;
4542 let (addr_tx, addr_rx) = mpsc::channel();
4543 // Receiver result: (net_events, peer_pmtu seen, local_pmtu reported).
4544 let (rres_tx, rres_rx) = mpsc::channel();
4545
4546 let rx = std::thread::spawn(move || {
4547 let mut recv = ReliableUdpReceiver::bind("127.0.0.1:0").unwrap();
4548 // Force a known egress MTU and a path event on this (receiver) end.
4549 recv.inject_pmtu(1400);
4550 recv.inject_path_event();
4551 addr_tx.send(recv.local_addr().unwrap()).unwrap();
4552 let mut got = 0u64;
4553 let start = Instant::now();
4554 while got < n {
4555 if start.elapsed() > Duration::from_secs(20) {
4556 break;
4557 }
4558 for _item in recv.poll().unwrap() {
4559 got += 1;
4560 }
4561 }
4562 // Grace: keep the feedback flowing so the sender's heartbeat (with
4563 // its Pmtu frame) is drained and our own feedback Pmtu is sent.
4564 for _ in 0..60 {
4565 recv.nudge_feedback().ok();
4566 std::thread::sleep(Duration::from_millis(2));
4567 }
4568 rres_tx
4569 .send((recv.net_event_count(), recv.peer_pmtu(), recv.local_pmtu()))
4570 .unwrap();
4571 got
4572 });
4573
4574 let recv_addr = addr_rx.recv().unwrap();
4575 let (sres_tx, sres_rx) = mpsc::channel();
4576 let tx = std::thread::spawn(move || {
4577 let mut send = ReliableUdpSender::bind("127.0.0.1:0", recv_addr, 8, 2, 8).unwrap();
4578 // Force a distinct known egress MTU on the sender.
4579 send.inject_pmtu(1280);
4580 for i in 0..n {
4581 while send.flow_blocked() {
4582 send.drain_until_acked(Duration::from_millis(50)).ok();
4583 }
4584 send.send_item(&i.to_le_bytes()).unwrap();
4585 }
4586 send.flush().unwrap();
4587 send.drain_until_acked(Duration::from_secs(15)).unwrap();
4588 // Drain the receiver's feedback so its Pmtu frame lands here.
4589 for _ in 0..60 {
4590 send.pump_feedback().ok();
4591 std::thread::sleep(Duration::from_millis(2));
4592 }
4593 sres_tx
4594 .send((send.net_event_count(), send.peer_pmtu(), send.local_pmtu()))
4595 .unwrap();
4596 });
4597
4598 let got = rx.join().unwrap();
4599 tx.join().unwrap();
4600 assert_eq!(got, n, "all items delivered");
4601 let (recv_events, recv_peer_pmtu, recv_local_pmtu) = rres_rx.recv().unwrap();
4602 let (send_events, send_peer_pmtu, send_local_pmtu) = sres_rx.recv().unwrap();
4603 // The injected path event registered on the receiver.
4604 assert!(recv_events >= 1, "receiver path event registered");
4605 // The injected path event (MTU drop on the sender, plus the inject)
4606 // registered on the sender too.
4607 assert!(send_events >= 1, "sender path event registered");
4608 // Each endpoint's egress MTU rode its frame to the peer, faithfully.
4609 assert_eq!(
4610 send_peer_pmtu, recv_local_pmtu,
4611 "sender learned the receiver's MTU via the feedback Pmtu frame"
4612 );
4613 assert_eq!(
4614 recv_peer_pmtu, send_local_pmtu,
4615 "receiver learned the sender's MTU via the heartbeat Pmtu frame"
4616 );
4617 }
4618}