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