Skip to main content

subetha_cxc/
udp_bridge.rs

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