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