Skip to main content

phantom_protocol/api/
udp_transport.rs

1//! `SessionTransport` impls over raw UDP (PhantomUDP). `UdpClientTransport` is an
2//! unconnected-socket client (it `send_to`s a tracked server address and `recv_from`s any
3//! source, so it can hear — and later follow — a server that migrates to a new address);
4//! `UdpServerTransport` is a per-session shim fed by the listener demux that can migrate its
5//! own send socket. Both add / strip the outer `[flags][cid]` envelope exactly as
6//! `TcpSessionTransport` adds / strips its 4-byte length prefix, so `run_data_pump` /
7//! `run_client_handshake` / `drive_server_handshake` are reused.
8
9use crate::api::session::{FramePhase, SessionTransport};
10use crate::errors::CoreError;
11use crate::transport::phantom_udp::datagram::{encode_datagrams, push_datagram, FragmentAssembler};
12use crate::transport::phantom_udp::envelope::{ConnId, PacketType, PATH_MTU};
13// `HDR_LEN` is referenced only by the test module (`super::HDR_LEN`); a plain top-level
14// import trips clippy's `--lib` unused-import check, which excludes `#[cfg(test)]` code.
15#[cfg(test)]
16use crate::transport::phantom_udp::envelope::HDR_LEN;
17use arc_swap::ArcSwap;
18use bytes::Bytes;
19use std::net::SocketAddr;
20use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
21use std::sync::Arc;
22use std::time::Duration;
23use tokio::net::UdpSocket;
24use tokio::sync::{mpsc, Mutex};
25
26/// Retransmit timeout for the Handshake phase stop-and-wait shim.
27const HANDSHAKE_RTO: Duration = Duration::from_millis(400);
28/// Max Handshake-phase retransmits before giving up (the outer 10s connect deadline still applies).
29const MAX_HANDSHAKE_RETX: u32 = 6;
30
31const PHASE_HANDSHAKE: u8 = 0;
32const PHASE_ESTABLISHED: u8 = 1;
33
34/// Outcome of classifying a UDP `recv_from` result (M-6).
35enum RecvAction {
36    /// A datagram of this many bytes arrived from this source (the source is needed for
37    /// server-migration candidate detection — M-1).
38    Got(usize, SocketAddr),
39    /// An ICMP-induced *advisory* error — retry, do not tear the session down.
40    Retry,
41    /// A genuine, fatal socket error.
42    Fatal(std::io::Error),
43}
44
45/// Whether a UDP `recv` error is an ICMP-induced *advisory* condition (RFC 8085 §5.5 /
46/// RFC 9000 §14.2). The client socket is unconnected, so the kernel rarely attributes an
47/// ICMP error to a `recv_from` (it cannot map it to a specific unconnected send) — this
48/// handling is mostly dormant there but retained for the platforms that do surface one. A
49/// single such error — which an off-path attacker can forge (the UDP analogue of a forged
50/// RST) — must NOT kill the session.
51fn is_advisory_recv_error(e: &std::io::Error) -> bool {
52    use std::io::ErrorKind;
53    // ConnectionRefused (ICMP port-unreachable — the audit's M-6 case) and ConnectionReset are
54    // matched via the portable `ErrorKind`. Host/network-unreachable are matched by raw errno
55    // instead, so the check stays uniform across platforms regardless of whether the OS maps
56    // them to the named `ErrorKind`s (Linux: EHOSTUNREACH = 113, ENETUNREACH = 101).
57    if matches!(
58        e.kind(),
59        ErrorKind::ConnectionRefused | ErrorKind::ConnectionReset
60    ) {
61        return true;
62    }
63    #[cfg(target_os = "linux")]
64    if let Some(errno) = e.raw_os_error() {
65        return errno == 113 || errno == 101;
66    }
67    false
68}
69
70/// Classify a `recv_from` result: an advisory ICMP error is retried, a genuine error is
71/// fatal (M-6); a datagram carries its source for migration-candidate detection.
72fn classify_recv(r: std::io::Result<(usize, SocketAddr)>) -> RecvAction {
73    match r {
74        Ok((n, src)) => RecvAction::Got(n, src),
75        Err(e) if is_advisory_recv_error(&e) => RecvAction::Retry,
76        Err(e) => RecvAction::Fatal(e),
77    }
78}
79
80pub struct UdpClientTransport {
81    /// Active send/recv socket. `ArcSwap` so `migrate()` can atomically rebind to a
82    /// new local socket (Phase 4 / P4.2b) without re-handshake; `send_bytes` and the
83    /// recv loop reload it per call.
84    socket: ArcSwap<UdpSocket>,
85    /// The previous socket, retained during a migration overlap so the client keeps
86    /// receiving downstream data on the old path until the new path shows life
87    /// (D7 / broken-rebind safety). `None` outside an overlap; dropped on the first
88    /// frame received on the new socket.
89    prev_socket: ArcSwap<Option<Arc<UdpSocket>>>,
90    /// The server remote this client `send_to`s. `ArcSwap` because the socket is
91    /// unconnected (the destination is explicit per send rather than a kernel-pinned connect
92    /// peer) AND because a server-migration *follow* re-points it to a validated new server
93    /// address without a re-handshake: when the server moves, the client path-validates the
94    /// new source and [`promote_candidate`](SessionTransport::promote_candidate) stores it
95    /// here, so subsequent c2s flows to the new address. Read on every `send_bytes`.
96    server_addr: ArcSwap<SocketAddr>,
97    /// The bootstrap (handshake) ConnId — a random lifetime id stamped until the
98    /// session sets the rotating chain via [`set_outbound_cid`](SessionTransport::set_outbound_cid).
99    cid: ConnId,
100    /// The rotating routing CID set at the handshake → data-pump boundary (ε /
101    /// WIRE v5). `None` during the handshake (the bootstrap `cid` is stamped);
102    /// `Some(CID_0)` once the session sets it, after which every datagram stamps it.
103    established_cid: ArcSwap<Option<ConnId>>,
104    phase: AtomicU8,
105    next_packet_id: AtomicU32,
106    /// Datagrams of the most recently sent frame, retransmitted on RTO during Handshake.
107    last_sent: Mutex<Vec<Vec<u8>>>,
108    reasm: Mutex<FragmentAssembler>,
109    /// Server-migration candidate (the mirror of the server's client-migration candidate): a
110    /// NEW server source (≠ `server_addr`) observed for this session. The session
111    /// path-validates it before any switch; `promote_candidate` then stores it as the new
112    /// `server_addr`. `None` until a migrated server's source appears.
113    candidate: ArcSwap<Option<SocketAddr>>,
114    /// Anti-amplification budget for the candidate (D9, RFC 9000 §8.2): bytes received from /
115    /// sent to the candidate, so a `PATH_CHALLENGE` to a possibly-spoofed new server source
116    /// never exceeds 3× what it sent us. Bounds the redirection/reflection an on-path
117    /// attacker (replaying a fresh frame with a spoofed source) could induce.
118    cand_recv: AtomicU64,
119    cand_sent: AtomicU64,
120    /// Source + byte length of the most recent `recv_bytes` frame (M-1). The candidate is
121    /// committed from this ONLY by `confirm_authenticated_source` on the post-decrypt path,
122    /// so a spoofed / replayed datagram (which fails AEAD or the replay window before
123    /// `confirm_authenticated_source` runs) can never clobber the candidate slot.
124    last_recv_src: ArcSwap<Option<SocketAddr>>,
125    last_frame_len: AtomicU64,
126}
127
128impl UdpClientTransport {
129    /// Bind a fresh UNCONNECTED UDP socket for talking to `server`, choosing a random
130    /// lifetime connection-ID. The socket is left unconnected (no `socket.connect`) so the
131    /// client can `recv_from` any source — the precondition for hearing, and later
132    /// following, a server that migrates to a new address. Datagrams are sent with
133    /// `send_to(server_addr)`.
134    pub async fn connect(server: SocketAddr) -> Result<Self, CoreError> {
135        let bind = if server.is_ipv4() {
136            "0.0.0.0:0"
137        } else {
138            "[::]:0"
139        };
140        let socket = UdpSocket::bind(bind)
141            .await
142            .map_err(|e| CoreError::NetworkError(format!("udp bind: {e}")))?;
143        let mut cid = [0u8; 8];
144        getrandom::getrandom(&mut cid).map_err(|e| CoreError::RngError(e.to_string()))?;
145        Ok(Self {
146            socket: ArcSwap::from_pointee(socket),
147            prev_socket: ArcSwap::from_pointee(None),
148            server_addr: ArcSwap::from_pointee(server),
149            cid,
150            established_cid: ArcSwap::from_pointee(None),
151            phase: AtomicU8::new(PHASE_HANDSHAKE),
152            next_packet_id: AtomicU32::new(0),
153            last_sent: Mutex::new(Vec::new()),
154            reasm: Mutex::new(FragmentAssembler::new()),
155            candidate: ArcSwap::from_pointee(None),
156            cand_recv: AtomicU64::new(0),
157            cand_sent: AtomicU64::new(0),
158            last_recv_src: ArcSwap::from_pointee(None),
159            last_frame_len: AtomicU64::new(0),
160        })
161    }
162
163    /// Rebind to a fresh local socket and route subsequent traffic through it
164    /// (Phase 4 / P4.2b — embedder-triggered migration). Best-effort and
165    /// non-blocking on validation: it binds a fresh unconnected socket (sends still
166    /// `send_to` the tracked `server_addr`), then atomically swaps it in as the active
167    /// socket while KEEPING the old one for the overlap (the recv loop listens on both
168    /// until the new path shows
169    /// life, then drops the old — broken-rebind safety / D7). Subsequent app data +
170    /// L1 retransmits go out the new socket, so the server detects the new source
171    /// (P4.1) and challenges → validates → swaps its peer. The caller is responsible
172    /// for bumping the session send `path_id` ([`Session::next_migration_path_id`]).
173    ///
174    /// A bind/connect failure returns `Err` WITHOUT touching the active socket, so a
175    /// migration to a dead/invalid address never tears the session down — the session
176    /// keeps running on the old socket.
177    ///
178    /// Typed core of the migration; the SocketAddr-free [`SessionTransport::migrate`]
179    /// trait entry parses a `String` and delegates here.
180    ///
181    /// [`Session::next_migration_path_id`]: crate::transport::session::Session::next_migration_path_id
182    pub async fn migrate_to(&self, new_local_addr: SocketAddr) -> Result<(), CoreError> {
183        let new_sock = UdpSocket::bind(new_local_addr)
184            .await
185            .map_err(|e| CoreError::NetworkError(format!("udp migrate bind: {e}")))?;
186        // The socket stays UNCONNECTED (sends use `send_to(server_addr)`), so there is no
187        // `connect` step — the send target is the tracked `server_addr`, unchanged by a
188        // local rebind.
189        let new_sock = Arc::new(new_sock);
190        // Retain the old socket FIRST (so the recv loop never sees a gap), then swap
191        // the active socket. The recv loop drops the retained socket on the first
192        // frame it receives on the new one.
193        let old = self.socket.load_full();
194        self.prev_socket.store(Arc::new(Some(old)));
195        self.socket.store(new_sock);
196        Ok(())
197    }
198
199    /// The connection-ID this client stamps on every datagram (test/inspection helper).
200    // Only called from the `#[cfg(test)]` module; `--lib` clippy excludes test code, so the
201    // dead-code lint would fire without this allow.
202    #[allow(dead_code)]
203    pub(crate) fn cid(&self) -> ConnId {
204        self.cid
205    }
206
207    /// Whether a post-migration dual-socket overlap is currently active (test/inspection
208    /// helper): `true` between a `migrate_to` and the first well-formed datagram on the new
209    /// socket that retires the old one.
210    #[cfg(test)]
211    pub(crate) fn in_migration_overlap(&self) -> bool {
212        self.prev_socket.load().is_some()
213    }
214
215    fn pkt_id(&self) -> u32 {
216        self.next_packet_id.fetch_add(1, Ordering::Relaxed)
217    }
218}
219
220impl SessionTransport for UdpClientTransport {
221    async fn send_bytes(&self, data: &[u8]) -> Result<(), CoreError> {
222        // Handshake frames are Initial (long header); post-handshake frames are OneRtt (short header).
223        let ty = if self.phase.load(Ordering::Relaxed) == PHASE_HANDSHAKE {
224            PacketType::Initial
225        } else {
226            PacketType::OneRtt
227        };
228        // ε / WIRE v5: stamp the rotating CID once the handshake set it; the
229        // bootstrap `cid` is stamped until then.
230        let cid = (**self.established_cid.load()).unwrap_or(self.cid);
231        let dgrams = encode_datagrams(ty, &cid, self.pkt_id(), data)
232            .map_err(|e| CoreError::NetworkError(format!("frame too large to fragment: {e}")))?;
233        // Snapshot the active socket (owned `Arc`, not a `Guard`) so we never hold an
234        // `ArcSwap` guard across `.await` — `migrate()` can swap it concurrently.
235        let sock = self.socket.load_full();
236        // Unconnected socket: the destination is explicit. `server_addr` is the tracked
237        // server remote (a later server-migration follow can re-point it without rebinding).
238        let dst = **self.server_addr.load();
239        for d in &dgrams {
240            sock.send_to(d, dst)
241                .await
242                .map_err(|e| CoreError::NetworkError(format!("udp send: {e}")))?;
243        }
244        // Remember for Handshake-phase retransmit (ignored once Established).
245        *self.last_sent.lock().await = dgrams;
246        Ok(())
247    }
248
249    async fn recv_bytes(&self) -> Result<Bytes, CoreError> {
250        // Sized at PATH_MTU + slack. We only ever emit datagrams <= PATH_MTU, so a legitimate peer
251        // never exceeds this; an oversized datagram is truncated by `recv_from` and then dropped by
252        // the `decode_header`/reassembly failure path below — intentional.
253        let mut buf = vec![0u8; PATH_MTU + 64];
254        // Second recv buffer, lazily sized only during a migration overlap; the common
255        // no-migration path keeps the single `buf`.
256        let mut buf_prev: Vec<u8> = Vec::new();
257        let mut retx = 0u32;
258        loop {
259            let in_handshake = self.phase.load(Ordering::Relaxed) == PHASE_HANDSHAKE;
260            // Snapshot both sockets as owned `Arc`s (never hold an `ArcSwap` guard
261            // across `.await`). `prev_opt` is `Some` only during a post-handshake
262            // migration overlap; we then listen on BOTH the new (active) socket and the
263            // retained old one (D7 / broken-rebind safety) until the new path shows
264            // life, then drop the old.
265            let active = self.socket.load_full();
266            let prev_arc = self.prev_socket.load_full();
267            let prev_opt: Option<Arc<UdpSocket>> = (*prev_arc).clone();
268            // The socket is unconnected, so `recv_from` returns the source of every datagram;
269            // we deliver from ANY source (the inner AEAD + replay window are the real guards,
270            // exactly as the server delivers any CID-matched datagram via its demux). During a
271            // migration overlap, ANY well-formed datagram on the NEW socket means the path is
272            // up — the overlap-drop after `push_datagram` (below) retires the old socket then.
273
274            // `from_prev` records which buffer the datagram landed in, so we slice the
275            // right one AFTER the select — the recv future's `&mut buf` borrow is
276            // released when its arm wins, exactly as the single-socket path relied on.
277            let (n, from_prev, src): (usize, bool, SocketAddr) = if in_handshake {
278                // Migration is post-handshake only, so there is never a `prev` socket
279                // here; keep the original single-socket + RTO-retransmit logic.
280                let server = **self.server_addr.load();
281                tokio::select! {
282                    // `biased;` polls the recv arm first: the RTO must be a true
283                    // "no data arrived for HANDSHAKE_RTO" timer, not a coin-flip against an
284                    // already-queued datagram. With the default unbiased select, when BOTH a datagram
285                    // is ready AND the sleep has elapsed (common under contention from concurrent PQ
286                    // handshakes), the recv arm is starved ~50% of the time, so the client spuriously
287                    // retransmits instead of processing the already-arrived ServerHello — exhausting
288                    // MAX_HANDSHAKE_RETX and timing the handshake out. Biasing toward received data
289                    // makes the RTO fire only when recv is genuinely pending.
290                    biased;
291                    r = active.recv_from(&mut buf) => match classify_recv(r) {
292                        RecvAction::Got(n, src) => (n, false, src),
293                        RecvAction::Retry => {
294                            log::debug!("PhantomUDP: advisory recv error (ignored, RFC 8085 §5.5)");
295                            continue;
296                        }
297                        RecvAction::Fatal(e) => {
298                            return Err(CoreError::NetworkError(format!("udp recv: {e}")))
299                        }
300                    },
301                    _ = tokio::time::sleep(HANDSHAKE_RTO) => {
302                        retx += 1;
303                        if retx > MAX_HANDSHAKE_RETX {
304                            return Err(CoreError::Timeout);
305                        }
306                        for d in self.last_sent.lock().await.iter() {
307                            let _ = active.send_to(d, server).await;
308                        }
309                        continue;
310                    }
311                }
312            } else if let Some(prev_sock) = &prev_opt {
313                if buf_prev.len() < PATH_MTU + 64 {
314                    buf_prev.resize(PATH_MTU + 64, 0);
315                }
316                tokio::select! {
317                    r = active.recv_from(&mut buf) => match classify_recv(r) {
318                        RecvAction::Got(n, src) => (n, false, src),
319                        RecvAction::Retry => {
320                            log::debug!("PhantomUDP: advisory recv error on new path (ignored)");
321                            continue;
322                        }
323                        RecvAction::Fatal(e) => {
324                            return Err(CoreError::NetworkError(format!("udp recv: {e}")))
325                        }
326                    },
327                    r = prev_sock.recv_from(&mut buf_prev) => match classify_recv(r) {
328                        RecvAction::Got(n, src) => (n, true, src),
329                        RecvAction::Retry => {
330                            log::debug!("PhantomUDP: advisory recv error on old path (ignored)");
331                            continue;
332                        }
333                        RecvAction::Fatal(e) => {
334                            return Err(CoreError::NetworkError(format!("udp recv: {e}")))
335                        }
336                    },
337                }
338            } else {
339                match classify_recv(active.recv_from(&mut buf).await) {
340                    RecvAction::Got(n, src) => (n, false, src),
341                    RecvAction::Retry => {
342                        log::debug!("PhantomUDP: advisory recv error (ignored, RFC 8085 §5.5)");
343                        continue;
344                    }
345                    RecvAction::Fatal(e) => {
346                        return Err(CoreError::NetworkError(format!("udp recv: {e}")))
347                    }
348                }
349            };
350            retx = 0; // progress: reset the RTO budget
351            let datagram = if from_prev { &buf_prev[..n] } else { &buf[..n] };
352            let mut asm = self.reasm.lock().await;
353            let decoded = push_datagram(&mut asm, datagram);
354            // Overlap-drop (D7): a well-formed datagram on the NEW (active) socket means the
355            // path is up, so retire the retained old socket — regardless of source, so it
356            // works whether the server is reaching us at the established address OR has
357            // itself migrated to a new one (a `src == server_addr` check would never fire in
358            // the latter case and strand the overlap). Garbage spray (a decode `Err`) does
359            // NOT end the overlap; data on the OLD socket (`from_prev`) never does either.
360            if !from_prev && prev_opt.is_some() && decoded.is_ok() {
361                self.prev_socket.store(Arc::new(None));
362            }
363            match decoded {
364                Ok((_hdr, Some(frame))) => {
365                    // M-1 (server-migration candidate detection): record the source + frame
366                    // length of this still-AEAD-pending frame. The candidate is committed ONLY
367                    // by the post-decrypt `confirm_authenticated_source`, so a spoofed / replayed
368                    // datagram (rejected by AEAD or the replay window before that runs) can never
369                    // clobber the candidate slot. Mirrors `UdpServerTransport::recv_bytes`.
370                    self.last_recv_src.store(Arc::new(Some(src)));
371                    self.last_frame_len
372                        .store(frame.len() as u64, Ordering::Relaxed);
373                    return Ok(Bytes::from(frame));
374                }
375                Ok((_hdr, None)) => continue, // partial fragment; keep receiving
376                Err(_) => continue,           // malformed datagram; drop and keep receiving
377            }
378        }
379    }
380
381    fn set_frame_phase(&self, phase: FramePhase) {
382        let v = match phase {
383            FramePhase::Handshake => PHASE_HANDSHAKE,
384            FramePhase::Established => PHASE_ESTABLISHED,
385        };
386        self.phase.store(v, Ordering::Relaxed);
387    }
388
389    fn set_outbound_cid(&self, cid: [u8; 8]) {
390        self.established_cid.store(Arc::new(Some(cid)));
391    }
392
393    /// SocketAddr-free trait entry for connection migration (Phase 4 / P4.2c). Parses
394    /// the embedder-supplied local bind address and delegates to the typed
395    /// [`migrate_to`](Self::migrate_to). A malformed address is a clean `Err` that
396    /// leaves the session on its existing socket (best-effort, never fatal).
397    async fn migrate(&self, local_addr: String) -> Result<(), CoreError> {
398        let addr: SocketAddr = local_addr.parse().map_err(|e| {
399            CoreError::NetworkError(format!("migrate: bad local addr '{local_addr}': {e}"))
400        })?;
401        self.migrate_to(addr).await
402    }
403
404    // ── Server-migration follow (the client side of A2a) ──────────────────────
405    //
406    // The exact mirror of `UdpServerTransport`'s client-migration candidate machinery,
407    // with the roles flipped: here the candidate is a NEW SERVER source, and a promotion
408    // re-points `server_addr` (the c2s send target). The shared recv-path
409    // (`handle_packet`) drives these the same way for both peers — it commits the
410    // candidate post-AEAD (`confirm_authenticated_source`), challenges it under the 3×
411    // anti-amplification cap (`send_to_candidate`), and promotes it on a valid
412    // PATH_RESPONSE (`promote_candidate`). Anti-spoof holds exactly as on the server: a
413    // spoofed / replayed datagram is rejected by AEAD or the replay window BEFORE
414    // `confirm_authenticated_source` runs, so it can never become the candidate; and the
415    // switch happens only after the candidate echoes a path-validation challenge, so an
416    // on-path attacker that rewrites a fresh frame's source to a victim can at most induce
417    // a bounded (≤3×) PATH_CHALLENGE to that victim, never a c2s redirection.
418
419    fn confirm_authenticated_source(&self) {
420        // M-1: the frame from `last_recv_src` just authenticated (AEAD-opened, non-replayed),
421        // so it really is the server — possibly at a NEW address (the server migrated). Commit
422        // it as the candidate the session path-validates before switching, and (re)seed its
423        // anti-amplification budget. A spoofed source never reaches here.
424        let src = match **self.last_recv_src.load() {
425            Some(s) => s,
426            None => return,
427        };
428        if src == **self.server_addr.load() {
429            return; // already the established server — not a new path
430        }
431        let len = self.last_frame_len.load(Ordering::Relaxed);
432        if self.candidate.load().as_ref() == &Some(src) {
433            self.cand_recv.fetch_add(len, Ordering::Relaxed);
434        } else {
435            self.candidate.store(Arc::new(Some(src)));
436            self.cand_recv.store(len, Ordering::Relaxed);
437            self.cand_sent.store(0, Ordering::Relaxed);
438        }
439    }
440
441    fn has_migration_candidate(&self) -> bool {
442        self.candidate.load().is_some()
443    }
444
445    async fn send_to_candidate(&self, data: &[u8]) -> Result<bool, CoreError> {
446        let cand = self.candidate.load();
447        let addr = match cand.as_ref() {
448            Some(a) => *a,
449            None => return Ok(false),
450        };
451        let pid = self.next_packet_id.fetch_add(1, Ordering::Relaxed);
452        // Bootstrap cid (not the rotating `established_cid`): a challenge to a possibly-spoofed
453        // address must not leak the keyed rotating CID, mirroring the server.
454        let dgrams = encode_datagrams(PacketType::OneRtt, &self.cid, pid, data)
455            .map_err(|e| CoreError::NetworkError(format!("challenge too large: {e}")))?;
456        let wire: u64 = dgrams.iter().map(|d| d.len() as u64).sum();
457        // Anti-amplification (D9, RFC 9000 §8.2): never send > 3× what the candidate sent us.
458        let recv = self.cand_recv.load(Ordering::Relaxed);
459        if self.cand_sent.load(Ordering::Relaxed).saturating_add(wire) > recv.saturating_mul(3) {
460            return Ok(false);
461        }
462        let sock = self.socket.load_full();
463        for d in &dgrams {
464            sock.send_to(d, addr)
465                .await
466                .map_err(|e| CoreError::NetworkError(format!("udp send_to candidate: {e}")))?;
467        }
468        self.cand_sent.fetch_add(wire, Ordering::Relaxed);
469        Ok(true)
470    }
471
472    fn promote_candidate(&self) -> bool {
473        let cand = self.candidate.load();
474        match cand.as_ref() {
475            Some(addr) => {
476                // The candidate's path validated: re-point the c2s send target to the new
477                // server address + clear the candidate / anti-amp budget. Subsequent
478                // `send_bytes` (and L1 retransmits) now flow to the migrated server.
479                self.server_addr.store(Arc::new(*addr));
480                self.candidate.store(Arc::new(None));
481                self.cand_recv.store(0, Ordering::Relaxed);
482                self.cand_sent.store(0, Ordering::Relaxed);
483                true
484            }
485            None => false,
486        }
487    }
488}
489
490/// Per-session server transport. The listener's demux task reassembles inbound datagrams and pushes
491/// the inner frames to `rx`; outbound frames are enveloped and sent to the captured `peer` from
492/// `send_socket`. A server migration ([`migrate_to`](Self::migrate_to)) swaps `send_socket` to a
493/// freshly-bound local socket (so the client sees a new s2c source) and spawns a recv loop on it
494/// that feeds the SAME `rx` channel via `tx`, so the client's c2s frames are delivered transparently
495/// once it switches its send target — while the listener demux keeps feeding `rx` on the old address
496/// during the overlap.
497pub struct UdpServerTransport {
498    /// Active send socket. Starts as the shared listener socket (so all sessions egress
499    /// the listen address) and is atomically swapped to a freshly-bound dedicated socket on
500    /// a server migration (`migrate_to`), changing this session's s2c source address.
501    send_socket: ArcSwap<UdpSocket>,
502    /// Sender half of this session's inbound channel (the demux holds a sibling clone for
503    /// the listener-routed path). A server migration's recv loop forwards frames it receives
504    /// on the new socket through this, so `recv_bytes` (reading `rx`) sees both the
505    /// listener-demuxed old path and the migrated new path during the overlap.
506    tx: mpsc::Sender<(Bytes, SocketAddr)>,
507    /// Handle to the current server-migration recv loop, if any. Aborted when a newer
508    /// migration replaces it (a stale socket from an earlier move) and on drop, so the
509    /// task and its socket do not leak across repeated migrations.
510    migrate_task: parking_lot::Mutex<Option<tokio::task::JoinHandle<()>>>,
511    /// Established peer. `ArcSwap` so the session can atomically switch it to a
512    /// validated migration candidate (Phase 4 / P4.2) without re-handshake.
513    peer: ArcSwap<SocketAddr>,
514    /// The bootstrap (handshake) ConnId, stamped until the session sets the
515    /// rotating chain via [`set_outbound_cid`](SessionTransport::set_outbound_cid).
516    cid: ConnId,
517    /// The rotating routing CID set at the handshake → data-pump boundary (ε /
518    /// WIRE v5). `None` during the handshake; `Some(CID_0)` once the session sets
519    /// it. Stamped on server→client datagrams so that direction also rotates.
520    established_cid: ArcSwap<Option<ConnId>>,
521    phase: AtomicU8,
522    next_packet_id: AtomicU32,
523    rx: Mutex<mpsc::Receiver<(Bytes, SocketAddr)>>,
524    /// Migration candidate (Phase 4, P4.1): a source address other than `peer`
525    /// observed for this CID. The session challenges it before any switch; the
526    /// switch itself (changing `peer`) is P4.2. `None` until a new source appears.
527    candidate: ArcSwap<Option<SocketAddr>>,
528    /// Anti-amplification budget for the candidate (D9, RFC 9000 §8.2): bytes
529    /// received from / sent to the candidate, so a challenge to a possibly-spoofed
530    /// address never exceeds 3× what it sent us.
531    cand_recv: AtomicU64,
532    cand_sent: AtomicU64,
533    /// Source + byte length of the most recent `recv_bytes` frame (M-1). The candidate is
534    /// committed from this ONLY by `confirm_authenticated_source` on the post-decrypt path, so
535    /// a spoofed (never-decrypting) datagram cannot clobber the candidate slot.
536    last_recv_src: ArcSwap<Option<SocketAddr>>,
537    last_frame_len: AtomicU64,
538}
539
540impl UdpServerTransport {
541    pub fn new(
542        socket: Arc<UdpSocket>,
543        peer: SocketAddr,
544        cid: ConnId,
545        tx: mpsc::Sender<(Bytes, SocketAddr)>,
546        rx: mpsc::Receiver<(Bytes, SocketAddr)>,
547    ) -> Self {
548        Self {
549            send_socket: ArcSwap::new(socket),
550            tx,
551            migrate_task: parking_lot::Mutex::new(None),
552            peer: ArcSwap::from_pointee(peer),
553            cid,
554            established_cid: ArcSwap::from_pointee(None),
555            phase: AtomicU8::new(PHASE_HANDSHAKE),
556            next_packet_id: AtomicU32::new(0),
557            rx: Mutex::new(rx),
558            candidate: ArcSwap::from_pointee(None),
559            cand_recv: AtomicU64::new(0),
560            cand_sent: AtomicU64::new(0),
561            last_recv_src: ArcSwap::from_pointee(None),
562            last_frame_len: AtomicU64::new(0),
563        }
564    }
565
566    /// Migrate the server's send path to a fresh local socket (the server-side mirror of
567    /// the client's [`UdpClientTransport::migrate_to`]). Binds a new unconnected socket,
568    /// swaps it in as the active send socket (so subsequent server→client datagrams egress
569    /// the new local address — the client sees a new s2c source and follows it), and spawns
570    /// a recv loop on it that reassembles datagrams and forwards complete frames into the
571    /// SAME inbound channel `recv_bytes` reads. The listener demux keeps feeding that channel
572    /// on the old (listen) address during the overlap, so c2s never drops; once the client
573    /// switches its send target to the new address (path validation, a later step), its
574    /// frames arrive on the new socket and flow through transparently.
575    ///
576    /// A bind failure returns `Err` WITHOUT touching the active send socket — a server
577    /// migration to an invalid address never tears the session down (best-effort, like the
578    /// client side). Typed core; the `SocketAddr`-free [`SessionTransport::migrate_server`]
579    /// trait entry parses a `String` and delegates here.
580    pub async fn migrate_to(&self, new_local_addr: SocketAddr) -> Result<(), CoreError> {
581        let new_sock = UdpSocket::bind(new_local_addr)
582            .await
583            .map_err(|e| CoreError::NetworkError(format!("udp server migrate bind: {e}")))?;
584        let new_sock = Arc::new(new_sock);
585        // The recv loop and the send path share the same socket: the client sends c2s to
586        // the new server address (where the loop listens) and the server's s2c egress that
587        // same socket, so the client's source filter sees a single new server source.
588        let recv_sock = new_sock.clone();
589        let tx = self.tx.clone();
590        let task = tokio::spawn(async move {
591            Self::run_migration_recv(recv_sock, tx).await;
592        });
593        // Abort any prior migration recv loop (a stale socket from an earlier move) before
594        // publishing the new one, so tasks/sockets do not accumulate across migrations.
595        if let Some(old) = self.migrate_task.lock().replace(task) {
596            old.abort();
597        }
598        self.send_socket.store(new_sock);
599        Ok(())
600    }
601
602    /// Recv loop for a migrated server send socket: reassemble datagrams and forward complete
603    /// frames into the session's inbound channel (tagged with the source, M-1), exactly as the
604    /// listener demux does for the old path. Exits when the channel is closed (session gone)
605    /// or on a fatal socket error; an advisory ICMP error is retried.
606    async fn run_migration_recv(sock: Arc<UdpSocket>, tx: mpsc::Sender<(Bytes, SocketAddr)>) {
607        let mut asm = FragmentAssembler::new();
608        let mut buf = vec![0u8; PATH_MTU + 64];
609        loop {
610            let (n, src) = match sock.recv_from(&mut buf).await {
611                Ok(v) => v,
612                Err(e) if is_advisory_recv_error(&e) => continue,
613                Err(_) => return, // fatal socket error — stop this migration recv loop
614            };
615            match push_datagram(&mut asm, &buf[..n]) {
616                Ok((_hdr, Some(frame))) => {
617                    // A full channel drops the datagram (the peer retransmits); a closed
618                    // channel means the session ended → stop the loop.
619                    if tx.try_send((Bytes::from(frame), src)).is_err() && tx.is_closed() {
620                        return;
621                    }
622                }
623                Ok((_hdr, None)) => {} // partial fragment buffered
624                Err(_) => {}           // malformed datagram — drop and keep receiving
625            }
626        }
627    }
628}
629
630impl Drop for UdpServerTransport {
631    fn drop(&mut self) {
632        // Stop any in-flight server-migration recv loop so its task + socket are reclaimed
633        // even if no further datagram ever arrives to trip the channel-closed check.
634        if let Some(h) = self.migrate_task.get_mut().take() {
635            h.abort();
636        }
637    }
638}
639
640impl SessionTransport for UdpServerTransport {
641    async fn send_bytes(&self, data: &[u8]) -> Result<(), CoreError> {
642        let ty = if self.phase.load(Ordering::Relaxed) == PHASE_HANDSHAKE {
643            PacketType::Initial
644        } else {
645            PacketType::OneRtt
646        };
647        let pid = self.next_packet_id.fetch_add(1, Ordering::Relaxed);
648        // ε / WIRE v5: stamp the rotating CID once the handshake set it.
649        let cid = (**self.established_cid.load()).unwrap_or(self.cid);
650        let dgrams = encode_datagrams(ty, &cid, pid, data)
651            .map_err(|e| CoreError::NetworkError(format!("frame too large to fragment: {e}")))?;
652        let peer = **self.peer.load();
653        // Snapshot the active send socket (owned `Arc`) so we never hold an `ArcSwap` guard
654        // across `.await` — a server `migrate_to` can swap it concurrently.
655        let sock = self.send_socket.load_full();
656        for d in &dgrams {
657            sock.send_to(d, peer)
658                .await
659                .map_err(|e| CoreError::NetworkError(format!("udp send_to: {e}")))?;
660        }
661        Ok(())
662    }
663
664    async fn recv_bytes(&self) -> Result<Bytes, CoreError> {
665        let (frame, src) = self
666            .rx
667            .lock()
668            .await
669            .recv()
670            .await
671            .ok_or(CoreError::ConnectionClosed)?;
672        // M-1: record the source + length but do NOT register a migration candidate here — this
673        // frame has not been AEAD-verified yet, and a spoofed CID-matched datagram looks
674        // identical at this point. The candidate is committed only from the post-decrypt
675        // `confirm_authenticated_source`, so a spoofed source cannot clobber the candidate slot
676        // and misdirect / stall a legitimate migration (Phase 4, P4.1 — detect-only, no switch).
677        self.last_recv_src.store(Arc::new(Some(src)));
678        self.last_frame_len
679            .store(frame.len() as u64, Ordering::Relaxed);
680        Ok(frame)
681    }
682
683    fn confirm_authenticated_source(&self) {
684        // M-1: the frame from `last_recv_src` just authenticated (AEAD-opened), so it really is
685        // the established peer — possibly at a NEW address (migration / NAT rebind). Register it
686        // as the candidate the session challenges before switching, and (re)seed its
687        // anti-amplification budget. A spoofed source never reaches here (its frame fails
688        // decrypt), so it can never become the candidate.
689        let src = match **self.last_recv_src.load() {
690            Some(s) => s,
691            None => return,
692        };
693        if src == **self.peer.load() {
694            return; // already the established peer — not a new path
695        }
696        let len = self.last_frame_len.load(Ordering::Relaxed);
697        if self.candidate.load().as_ref() == &Some(src) {
698            self.cand_recv.fetch_add(len, Ordering::Relaxed);
699        } else {
700            self.candidate.store(Arc::new(Some(src)));
701            self.cand_recv.store(len, Ordering::Relaxed);
702            self.cand_sent.store(0, Ordering::Relaxed);
703        }
704    }
705
706    fn has_migration_candidate(&self) -> bool {
707        self.candidate.load().is_some()
708    }
709
710    async fn send_to_candidate(&self, data: &[u8]) -> Result<bool, CoreError> {
711        let cand = self.candidate.load();
712        let addr = match cand.as_ref() {
713            Some(a) => *a,
714            None => return Ok(false),
715        };
716        let pid = self.next_packet_id.fetch_add(1, Ordering::Relaxed);
717        let dgrams = encode_datagrams(PacketType::OneRtt, &self.cid, pid, data)
718            .map_err(|e| CoreError::NetworkError(format!("challenge too large: {e}")))?;
719        let wire: u64 = dgrams.iter().map(|d| d.len() as u64).sum();
720        // Anti-amplification (D9, RFC 9000 §8.2): never send > 3× what the
721        // candidate sent us. Drop the challenge rather than become a reflector.
722        let recv = self.cand_recv.load(Ordering::Relaxed);
723        if self.cand_sent.load(Ordering::Relaxed).saturating_add(wire) > recv.saturating_mul(3) {
724            return Ok(false);
725        }
726        let sock = self.send_socket.load_full();
727        for d in &dgrams {
728            sock.send_to(d, addr)
729                .await
730                .map_err(|e| CoreError::NetworkError(format!("udp send_to candidate: {e}")))?;
731        }
732        self.cand_sent.fetch_add(wire, Ordering::Relaxed);
733        Ok(true)
734    }
735
736    fn promote_candidate(&self) -> bool {
737        let cand = self.candidate.load();
738        match cand.as_ref() {
739            Some(addr) => {
740                // Switch the active peer to the validated candidate; clear the
741                // candidate + its anti-amp budget. Subsequent send_bytes + ARQ
742                // retransmits now target the new address.
743                self.peer.store(Arc::new(*addr));
744                self.candidate.store(Arc::new(None));
745                self.cand_recv.store(0, Ordering::Relaxed);
746                self.cand_sent.store(0, Ordering::Relaxed);
747                true
748            }
749            None => false,
750        }
751    }
752
753    fn set_frame_phase(&self, phase: FramePhase) {
754        let v = match phase {
755            FramePhase::Handshake => PHASE_HANDSHAKE,
756            FramePhase::Established => PHASE_ESTABLISHED,
757        };
758        self.phase.store(v, Ordering::Relaxed);
759    }
760
761    fn set_outbound_cid(&self, cid: [u8; 8]) {
762        self.established_cid.store(Arc::new(Some(cid)));
763    }
764
765    /// SocketAddr-free trait entry for server-side migration. Parses the new local bind
766    /// address and delegates to the typed [`migrate_to`](Self::migrate_to). A malformed
767    /// address is a clean `Err` that leaves the session on its existing send socket
768    /// (best-effort, never fatal).
769    async fn migrate_server(&self, local_addr: String) -> Result<(), CoreError> {
770        let addr: SocketAddr = local_addr.parse().map_err(|e| {
771            CoreError::NetworkError(format!(
772                "migrate_server: bad local addr '{local_addr}': {e}"
773            ))
774        })?;
775        self.migrate_to(addr).await
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782    use crate::transport::phantom_udp::datagram::{push_datagram, FragmentAssembler};
783    use crate::transport::phantom_udp::envelope::PacketType;
784    use tokio::net::UdpSocket;
785
786    /// A framed frame round-trips client -> raw peer -> client, including a >MTU
787    /// (fragmented) reply that `recv_bytes` reassembles.
788    #[tokio::test]
789    async fn client_send_recv_with_fragmented_reply() {
790        let peer = UdpSocket::bind("127.0.0.1:0").await.unwrap();
791        let peer_addr = peer.local_addr().unwrap();
792        let client = UdpClientTransport::connect(peer_addr).await.unwrap();
793
794        // Client sends a small frame.
795        client.send_bytes(b"hello").await.unwrap();
796        let mut buf = vec![0u8; 2048];
797        let (n, from) = peer.recv_from(&mut buf).await.unwrap();
798        let mut asm = FragmentAssembler::new();
799        let (_h, got) = push_datagram(&mut asm, &buf[..n]).unwrap();
800        assert_eq!(got.as_deref(), Some(&b"hello"[..]));
801
802        // Peer replies with a >MTU frame (fragments); client reassembles via recv_bytes.
803        let big: Vec<u8> = (0..5000u32).map(|i| i as u8).collect();
804        for d in encode_datagrams(PacketType::OneRtt, &client.cid(), 1, &big).expect("encode") {
805            peer.send_to(&d, from).await.unwrap();
806        }
807        let recv = tokio::time::timeout(std::time::Duration::from_secs(2), client.recv_bytes())
808            .await
809            .expect("no timeout")
810            .expect("recv");
811        assert_eq!(&recv[..], &big[..]);
812    }
813
814    /// D1 (server migration): the client uses an UNCONNECTED socket, so it can hear a
815    /// datagram from a source *other* than its original connect target — the precondition
816    /// for following a server that has migrated its send address to a new socket. A
817    /// connected socket drops such a datagram at the kernel (wrong source); the unconnected
818    /// client must deliver it (the inner AEAD + replay window are the real guards).
819    #[tokio::test]
820    async fn client_hears_a_datagram_from_a_new_server_source() {
821        let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
822        let server_addr = server.local_addr().unwrap();
823        let client = UdpClientTransport::connect(server_addr).await.unwrap();
824        client.set_frame_phase(FramePhase::Established);
825
826        // The server learns the client's local address (so the new source can target it).
827        client.send_bytes(b"hello").await.unwrap();
828        let mut buf = vec![0u8; 2048];
829        let (_n, client_addr) = server.recv_from(&mut buf).await.unwrap();
830
831        // A DIFFERENT source (a migrated server's freshly-bound socket) sends a framed
832        // datagram to the client. On a connected socket the kernel drops it; the
833        // unconnected client must deliver it up through `recv_bytes`.
834        let migrated = UdpSocket::bind("127.0.0.1:0").await.unwrap();
835        for d in encode_datagrams(PacketType::OneRtt, &client.cid(), 1, b"from-new-source").unwrap()
836        {
837            migrated.send_to(&d, client_addr).await.unwrap();
838        }
839        let got = tokio::time::timeout(Duration::from_secs(2), client.recv_bytes())
840            .await
841            .expect("client must hear a new server source (unconnected socket)")
842            .expect("recv");
843        assert_eq!(&got[..], b"from-new-source");
844    }
845
846    /// While in Handshake phase, a dropped first datagram is retransmitted on RTO.
847    #[tokio::test]
848    async fn client_retransmits_handshake_on_rto() {
849        let peer = UdpSocket::bind("127.0.0.1:0").await.unwrap();
850        let peer_addr = peer.local_addr().unwrap();
851        let client = UdpClientTransport::connect(peer_addr).await.unwrap();
852        // default phase is Handshake.
853        let send = async {
854            client.send_bytes(b"flight1").await.unwrap();
855        };
856        let mut buf = vec![0u8; 2048];
857        // Peer ignores the first datagram, reads the retransmit.
858        let recv = async {
859            let _ = peer.recv_from(&mut buf).await.unwrap(); // drop #1
860            let (n, from) = peer.recv_from(&mut buf).await.unwrap(); // retransmit
861                                                                     // Reply so client's recv_bytes completes.
862            for d in
863                encode_datagrams(PacketType::Initial, &client.cid(), 0, b"reply").expect("encode")
864            {
865                peer.send_to(&d, from).await.unwrap();
866            }
867            n
868        };
869        let recv_client = async {
870            tokio::time::timeout(std::time::Duration::from_secs(3), client.recv_bytes()).await
871        };
872        let (_s, n, r) = tokio::join!(send, recv, recv_client);
873        assert!(n >= super::HDR_LEN);
874        assert_eq!(&r.unwrap().unwrap()[..], &b"reply"[..]);
875    }
876
877    #[tokio::test]
878    async fn server_transport_send_and_recv() {
879        use tokio::sync::mpsc;
880        let sock = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
881        let peer = UdpSocket::bind("127.0.0.1:0").await.unwrap();
882        let peer_addr = peer.local_addr().unwrap();
883        let (tx, rx) = mpsc::channel(8);
884        let st = UdpServerTransport::new(sock.clone(), peer_addr, [3u8; 8], tx.clone(), rx);
885
886        // recv_bytes returns frames pushed to the channel (as the demux would),
887        // tagged with the source address (here the established peer).
888        tx.send((Bytes::from_static(b"from-demux"), peer_addr))
889            .await
890            .unwrap();
891        assert_eq!(&st.recv_bytes().await.unwrap()[..], b"from-demux");
892
893        // send_bytes writes an enveloped datagram the raw peer can decode.
894        st.set_frame_phase(FramePhase::Established);
895        st.send_bytes(b"to-peer").await.unwrap();
896        let mut buf = vec![0u8; 2048];
897        let (n, _from) = peer.recv_from(&mut buf).await.unwrap();
898        let mut asm = FragmentAssembler::new();
899        let (hdr, got) = push_datagram(&mut asm, &buf[..n]).unwrap();
900        assert_eq!(hdr.cid, [3u8; 8]);
901        assert_eq!(got.as_deref(), Some(&b"to-peer"[..]));
902    }
903
904    /// P4.1: a frame from a source other than the established peer registers a
905    /// migration candidate; `send_to_candidate` reaches it under the 3×
906    /// anti-amplification cap (D9). No peer switch happens here (that is P4.2).
907    #[tokio::test]
908    async fn server_detects_candidate_and_caps_amplification() {
909        use tokio::sync::mpsc;
910        let sock = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
911        let peer = UdpSocket::bind("127.0.0.1:0")
912            .await
913            .unwrap()
914            .local_addr()
915            .unwrap();
916        let (tx, rx) = mpsc::channel(16);
917        let st = UdpServerTransport::new(sock.clone(), peer, [9u8; 8], tx.clone(), rx);
918
919        // The established peer is not a candidate, and there is nothing to send to.
920        tx.send((Bytes::from_static(b"hi"), peer)).await.unwrap();
921        let _ = st.recv_bytes().await.unwrap();
922        assert!(!st.has_migration_candidate(), "the peer is not a candidate");
923        assert!(
924            !st.send_to_candidate(b"x").await.unwrap(),
925            "no candidate => Ok(false)"
926        );
927
928        // A frame from a NEW source registers a candidate + seeds the 3× budget
929        // (10 received bytes here).
930        let cand_sock = UdpSocket::bind("127.0.0.1:0").await.unwrap();
931        let cand_addr = cand_sock.local_addr().unwrap();
932        tx.send((Bytes::from_static(b"0123456789"), cand_addr))
933            .await
934            .unwrap();
935        let _ = st.recv_bytes().await.unwrap();
936        // M-1: the candidate is committed only on the post-decrypt (authenticated) path.
937        st.confirm_authenticated_source();
938        assert!(
939            st.has_migration_candidate(),
940            "a new source must set a candidate"
941        );
942
943        // A challenge within budget is delivered to the candidate address.
944        assert!(
945            st.send_to_candidate(b"chal").await.unwrap(),
946            "first challenge is within the 3× budget"
947        );
948        let mut buf = vec![0u8; 2048];
949        let (n, _from) = cand_sock.recv_from(&mut buf).await.unwrap();
950        assert!(n > 0, "the challenge must reach the candidate socket");
951
952        // Keep challenging until the 3× anti-amplification cap blocks.
953        let mut blocked = false;
954        for _ in 0..50 {
955            if !st.send_to_candidate(b"chal").await.unwrap() {
956                blocked = true;
957                break;
958            }
959        }
960        assert!(
961            blocked,
962            "the 3× anti-amplification cap must eventually block"
963        );
964    }
965
966    /// P4.2: promote_candidate atomically switches the established peer to the
967    /// validated candidate; subsequent send_bytes targets the new address.
968    #[tokio::test]
969    async fn promote_candidate_switches_the_peer() {
970        use tokio::sync::mpsc;
971        let server_sock = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
972        let old_peer_sock = UdpSocket::bind("127.0.0.1:0").await.unwrap();
973        let old_peer = old_peer_sock.local_addr().unwrap();
974        let new_sock = UdpSocket::bind("127.0.0.1:0").await.unwrap();
975        let new_addr = new_sock.local_addr().unwrap();
976
977        let (tx, rx) = mpsc::channel(8);
978        let ust = UdpServerTransport::new(server_sock.clone(), old_peer, [7u8; 8], tx.clone(), rx);
979        ust.set_frame_phase(FramePhase::Established);
980
981        assert!(
982            !ust.promote_candidate(),
983            "no candidate => nothing to promote"
984        );
985
986        // A frame from a new source sets the candidate (once it authenticates — M-1).
987        tx.send((Bytes::from_static(b"hi"), new_addr))
988            .await
989            .unwrap();
990        let _ = ust.recv_bytes().await.unwrap();
991        ust.confirm_authenticated_source();
992        assert!(ust.has_migration_candidate());
993
994        // Pre-switch: send_bytes goes to the OLD peer.
995        ust.send_bytes(b"before").await.unwrap();
996        let mut buf = vec![0u8; 512];
997        let (n, _) =
998            tokio::time::timeout(Duration::from_secs(1), old_peer_sock.recv_from(&mut buf))
999                .await
1000                .expect("pre-switch data reaches the old peer")
1001                .unwrap();
1002        assert!(n > 0);
1003
1004        // Switch.
1005        assert!(ust.promote_candidate(), "candidate must be promoted");
1006        assert!(
1007            !ust.has_migration_candidate(),
1008            "candidate cleared after promotion"
1009        );
1010
1011        // Post-switch: send_bytes now goes to the NEW peer.
1012        ust.send_bytes(b"after").await.unwrap();
1013        let (n2, _) = tokio::time::timeout(Duration::from_secs(1), new_sock.recv_from(&mut buf))
1014            .await
1015            .expect("post-switch data reaches the new peer")
1016            .unwrap();
1017        assert!(n2 > 0);
1018    }
1019
1020    /// D2 (server migration): `migrate_to` binds a fresh local socket, switches the server's
1021    /// SEND socket to it (so server→client datagrams egress a new source the client follows),
1022    /// and spawns a recv loop on it feeding the SAME inbound channel `recv_bytes` reads — so
1023    /// once the peer sends c2s to the new server address, its frames are delivered
1024    /// transparently (the listener demux keeps the old path alive during the overlap).
1025    #[tokio::test]
1026    async fn server_migrate_to_switches_send_socket_and_receives_on_it() {
1027        use tokio::sync::mpsc;
1028        let listener_sock = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
1029        let listen_addr = listener_sock.local_addr().unwrap();
1030        let peer_sock = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1031        let peer = peer_sock.local_addr().unwrap();
1032        let (tx, rx) = mpsc::channel(16);
1033        let st = UdpServerTransport::new(listener_sock.clone(), peer, [4u8; 8], tx.clone(), rx);
1034        st.set_frame_phase(FramePhase::Established);
1035
1036        // Pre-migration: the server's s2c egresses the (shared) listener socket.
1037        st.send_bytes(b"pre").await.unwrap();
1038        let mut buf = vec![0u8; 2048];
1039        let (_n, src_pre) = peer_sock.recv_from(&mut buf).await.unwrap();
1040        assert_eq!(
1041            src_pre, listen_addr,
1042            "pre-migration s2c egresses the listener socket"
1043        );
1044
1045        // Migrate the server's send path to a fresh local socket.
1046        st.migrate_to("127.0.0.1:0".parse().unwrap())
1047            .await
1048            .expect("server migrate binds a new socket");
1049
1050        // Post-migration: the server's s2c egresses the NEW socket — a different source the
1051        // client's unconnected socket (D1) can hear and follow.
1052        st.send_bytes(b"post").await.unwrap();
1053        let (_n2, src_post) = peer_sock.recv_from(&mut buf).await.unwrap();
1054        assert_ne!(
1055            src_post, src_pre,
1056            "server migration changes the s2c source address"
1057        );
1058
1059        // The peer now sends c2s to the new server address; the migration recv loop must
1060        // forward it into the same channel `recv_bytes` reads.
1061        for d in
1062            encode_datagrams(PacketType::OneRtt, &[4u8; 8], 1, b"c2s-after-server-move").unwrap()
1063        {
1064            peer_sock.send_to(&d, src_post).await.unwrap();
1065        }
1066        let got = tokio::time::timeout(Duration::from_secs(2), st.recv_bytes())
1067            .await
1068            .expect("c2s on the migrated server socket reaches recv_bytes")
1069            .expect("recv");
1070        assert_eq!(&got[..], b"c2s-after-server-move");
1071    }
1072
1073    /// D3 (server-migration follow): the CLIENT mirrors the server's candidate machinery — a
1074    /// NEW server source becomes a candidate ONLY post-authentication (M-1), a `PATH_CHALLENGE`
1075    /// reaches it under the 3× anti-amplification cap, and `promote_candidate` re-points
1076    /// `server_addr` so subsequent c2s flows to the migrated server.
1077    #[tokio::test]
1078    async fn client_detects_server_candidate_and_promotes_to_new_server_addr() {
1079        let orig_server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1080        let orig_addr = orig_server.local_addr().unwrap();
1081        let client = UdpClientTransport::connect(orig_addr).await.unwrap();
1082        client.set_frame_phase(FramePhase::Established);
1083
1084        assert!(!client.has_migration_candidate());
1085        assert!(
1086            !client.send_to_candidate(b"x").await.unwrap(),
1087            "no candidate => Ok(false)"
1088        );
1089
1090        // Learn the client's local address so the migrated server can target it.
1091        client.send_bytes(b"hi").await.unwrap();
1092        let mut buf = vec![0u8; 2048];
1093        let (_n, client_addr) = orig_server.recv_from(&mut buf).await.unwrap();
1094
1095        // A migrated server (a new source) sends a framed datagram; recv_bytes records it.
1096        let new_server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1097        for d in encode_datagrams(PacketType::OneRtt, &client.cid(), 1, b"0123456789").unwrap() {
1098            new_server.send_to(&d, client_addr).await.unwrap();
1099        }
1100        let _ = tokio::time::timeout(Duration::from_secs(2), client.recv_bytes())
1101            .await
1102            .expect("no timeout")
1103            .expect("recv");
1104        // M-1: a recv alone must NOT commit a candidate (it is not yet AEAD-verified).
1105        assert!(
1106            !client.has_migration_candidate(),
1107            "recv alone must NOT commit a candidate (M-1)"
1108        );
1109        client.confirm_authenticated_source();
1110        assert!(
1111            client.has_migration_candidate(),
1112            "an authenticated new server source sets the candidate"
1113        );
1114
1115        // A challenge reaches the candidate (the migrated server) within the 3× budget.
1116        assert!(
1117            client.send_to_candidate(b"chal").await.unwrap(),
1118            "first challenge is within the 3× budget"
1119        );
1120        let (cn, _) = new_server.recv_from(&mut buf).await.unwrap();
1121        assert!(cn > 0, "the challenge must reach the new server socket");
1122
1123        // Promote → `server_addr` switches to the new server; subsequent send_bytes go there.
1124        assert!(client.promote_candidate(), "candidate must be promoted");
1125        assert!(
1126            !client.has_migration_candidate(),
1127            "candidate cleared after promotion"
1128        );
1129        client.send_bytes(b"after").await.unwrap();
1130        let (an, _) = tokio::time::timeout(Duration::from_secs(1), new_server.recv_from(&mut buf))
1131            .await
1132            .expect("post-promote c2s reaches the migrated server")
1133            .unwrap();
1134        assert!(an > 0);
1135    }
1136
1137    /// Review finding (overlap-drop robustness): a client mid-(local)-migration overlap must
1138    /// retire its old socket on the first well-formed datagram on the NEW socket REGARDLESS
1139    /// of source — including from a server that has itself migrated to a new address. A
1140    /// `src == server_addr` check would never fire for a migrated server and strand the
1141    /// overlap (an idle extra socket for the session's life).
1142    #[tokio::test]
1143    async fn overlap_ends_on_data_from_a_migrated_server_source() {
1144        let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1145        let server_addr = server.local_addr().unwrap();
1146        let client = UdpClientTransport::connect(server_addr).await.unwrap();
1147        client.set_frame_phase(FramePhase::Established);
1148
1149        client.send_bytes(b"hi").await.unwrap();
1150        let mut buf = vec![0u8; 2048];
1151        let (_n, _src_old) = server.recv_from(&mut buf).await.unwrap();
1152
1153        // Enter a (client-local) migration overlap.
1154        client
1155            .migrate_to("127.0.0.1:0".parse().unwrap())
1156            .await
1157            .unwrap();
1158        assert!(
1159            client.in_migration_overlap(),
1160            "migrate_to enters the dual-socket overlap"
1161        );
1162
1163        // Learn the new client socket's address (so a third party can target it).
1164        client.send_bytes(b"probe").await.unwrap();
1165        let (_n2, client_new_addr) = server.recv_from(&mut buf).await.unwrap();
1166
1167        // A datagram from a DIFFERENT source than the original server (a migrated server's
1168        // fresh socket) arrives on the new socket — it must be delivered AND end the overlap.
1169        let migrated_server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1170        for d in encode_datagrams(
1171            PacketType::OneRtt,
1172            &client.cid(),
1173            1,
1174            b"from-migrated-server",
1175        )
1176        .unwrap()
1177        {
1178            migrated_server.send_to(&d, client_new_addr).await.unwrap();
1179        }
1180        let got = tokio::time::timeout(Duration::from_secs(2), client.recv_bytes())
1181            .await
1182            .expect("no timeout")
1183            .expect("recv");
1184        assert_eq!(&got[..], b"from-migrated-server");
1185        assert!(
1186            !client.in_migration_overlap(),
1187            "a well-formed datagram on the new socket (even from a migrated server source) \
1188             must end the overlap"
1189        );
1190    }
1191
1192    /// Review finding (H-1 reaping, refutation + guard): adding a `tx` clone to
1193    /// `UdpServerTransport` (so a server migration's recv loop can feed the same channel)
1194    /// does NOT break the demux's route reaping. `mpsc::Sender::is_closed()` tracks the
1195    /// RECEIVER being dropped, not the sender-clone count — so when the transport (which owns
1196    /// the `rx`) is dropped at session end, a sibling `tx` clone the demux retains for routing
1197    /// immediately observes `is_closed() == true` and the route is reclaimed.
1198    #[tokio::test]
1199    async fn dropping_the_transport_closes_the_demux_tx_clone() {
1200        use tokio::sync::mpsc;
1201        let sock = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
1202        let peer = UdpSocket::bind("127.0.0.1:0")
1203            .await
1204            .unwrap()
1205            .local_addr()
1206            .unwrap();
1207        let (tx, rx) = mpsc::channel(8);
1208        // The RouteTable retains a sibling clone for routing; the transport gets another.
1209        let demux_clone = tx.clone();
1210        let st = UdpServerTransport::new(sock, peer, [1u8; 8], tx.clone(), rx);
1211        assert!(
1212            !demux_clone.is_closed(),
1213            "a live session's route stays open"
1214        );
1215        // Session ends: the transport (holding `rx` + its own `tx` clone) is dropped.
1216        drop(st);
1217        assert!(
1218            demux_clone.is_closed(),
1219            "the demux's tx clone observes the dropped receiver (is_closed tracks the \
1220             receiver, not the sender-clone count), so the route is reaped — the transport's \
1221             extra tx clone does not strand it"
1222        );
1223    }
1224
1225    /// P4.2b: `migrate()` rebinds the client to a fresh (still unconnected) local socket
1226    /// that keeps `send_to`-ing the same `server_addr`, so the client's source address
1227    /// changes — which is what makes the server detect the new path (P4.1). The new socket
1228    /// becomes the active send/recv socket; a reply to the new source is received.
1229    #[tokio::test]
1230    async fn migrate_rebinds_to_a_new_local_socket() {
1231        let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1232        let server_addr = server.local_addr().unwrap();
1233        let client = UdpClientTransport::connect(server_addr).await.unwrap();
1234        client.set_frame_phase(FramePhase::Established);
1235
1236        // Pre-migration: the server sees the original source.
1237        client.send_bytes(b"pre").await.unwrap();
1238        let mut buf = vec![0u8; 2048];
1239        let (_n, src_old) = server.recv_from(&mut buf).await.unwrap();
1240
1241        // Migrate to a fresh ephemeral local socket.
1242        client
1243            .migrate_to("127.0.0.1:0".parse().unwrap())
1244            .await
1245            .expect("migrate binds a new socket");
1246
1247        // Post-migration: the server sees a DIFFERENT source (the new socket).
1248        client.send_bytes(b"post").await.unwrap();
1249        let (_n2, src_new) = server.recv_from(&mut buf).await.unwrap();
1250        assert_ne!(
1251            src_old, src_new,
1252            "migrate() must change the client's source address"
1253        );
1254
1255        // A reply to the new source is received on the new (active) socket.
1256        for d in encode_datagrams(PacketType::OneRtt, &client.cid(), 7, b"reply-new").unwrap() {
1257            server.send_to(&d, src_new).await.unwrap();
1258        }
1259        let got = tokio::time::timeout(Duration::from_secs(2), client.recv_bytes())
1260            .await
1261            .expect("no timeout")
1262            .expect("recv");
1263        assert_eq!(&got[..], b"reply-new");
1264    }
1265
1266    /// P4.2b: during the migration overlap the client keeps the OLD socket and still
1267    /// receives on it (broken-rebind safety / D7) — the server, until it validates +
1268    /// swaps, keeps sending downstream app data to the old address. The session must
1269    /// not lose that data.
1270    #[tokio::test]
1271    async fn migrate_keeps_receiving_on_the_old_socket_during_overlap() {
1272        let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1273        let server_addr = server.local_addr().unwrap();
1274        let client = UdpClientTransport::connect(server_addr).await.unwrap();
1275        client.set_frame_phase(FramePhase::Established);
1276
1277        client.send_bytes(b"hi").await.unwrap();
1278        let mut buf = vec![0u8; 2048];
1279        let (_n, src_old) = server.recv_from(&mut buf).await.unwrap();
1280
1281        client
1282            .migrate_to("127.0.0.1:0".parse().unwrap())
1283            .await
1284            .expect("migrate binds a new socket");
1285
1286        // The server has NOT yet validated/swapped, so it still sends downstream to
1287        // the OLD source. The client must still receive it on the retained old socket.
1288        for d in encode_datagrams(PacketType::OneRtt, &client.cid(), 1, b"downstream-old").unwrap()
1289        {
1290            server.send_to(&d, src_old).await.unwrap();
1291        }
1292        let got = tokio::time::timeout(Duration::from_secs(2), client.recv_bytes())
1293            .await
1294            .expect("no timeout")
1295            .expect("recv on retained old socket");
1296        assert_eq!(&got[..], b"downstream-old");
1297    }
1298
1299    /// P4.2c: the SocketAddr-free `migrate(String)` trait entry parses the address; a
1300    /// malformed address is a clean `Err` and leaves the session untouched on the old
1301    /// socket (best-effort, never fatal). The session keeps working afterwards.
1302    #[tokio::test]
1303    async fn migrate_with_a_bad_local_addr_is_a_clean_error() {
1304        let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
1305        let server_addr = server.local_addr().unwrap();
1306        let client = UdpClientTransport::connect(server_addr).await.unwrap();
1307        client.set_frame_phase(FramePhase::Established);
1308
1309        // Unparseable address → Err, no rebind (the SocketAddr-free trait entry).
1310        let err = client.migrate("not-an-address".to_string()).await;
1311        assert!(err.is_err(), "a malformed local addr must be a clean Err");
1312
1313        // The session still works on the original socket.
1314        client.send_bytes(b"still-alive").await.unwrap();
1315        let mut buf = vec![0u8; 2048];
1316        let (n, _from) = tokio::time::timeout(Duration::from_secs(2), server.recv_from(&mut buf))
1317            .await
1318            .expect("no timeout")
1319            .unwrap();
1320        assert!(
1321            n > 0,
1322            "data still flows on the original socket after a failed migrate"
1323        );
1324    }
1325
1326    /// M-1 (audit 2026-06-11): the migration candidate (the server's PATH_CHALLENGE target)
1327    /// must be registered only from an AEAD-AUTHENTICATED frame, not from any CID-matched
1328    /// datagram's raw source — else a spoofed source can clobber the single candidate slot and
1329    /// misdirect / stall a legitimate migration. `recv_bytes` records but does not commit; the
1330    /// post-decrypt `confirm_authenticated_source` commits.
1331    #[tokio::test]
1332    async fn candidate_is_set_only_from_an_authenticated_source() {
1333        use tokio::sync::mpsc;
1334        let sock = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
1335        let peer = UdpSocket::bind("127.0.0.1:0")
1336            .await
1337            .unwrap()
1338            .local_addr()
1339            .unwrap();
1340        let (tx, rx) = mpsc::channel(8);
1341        let st = UdpServerTransport::new(sock.clone(), peer, [5u8; 8], tx.clone(), rx);
1342
1343        // A frame from a NEW source arrives but has not yet been AEAD-verified (pre-decrypt) —
1344        // exactly what a spoofed CID-matched datagram looks like at recv time.
1345        let other = UdpSocket::bind("127.0.0.1:0")
1346            .await
1347            .unwrap()
1348            .local_addr()
1349            .unwrap();
1350        tx.send((Bytes::from_static(b"pre-decrypt-source"), other))
1351            .await
1352            .unwrap();
1353        let _ = st.recv_bytes().await.unwrap();
1354        assert!(
1355            !st.has_migration_candidate(),
1356            "a pre-decrypt (possibly spoofed) source must NOT set the migration candidate (M-1)"
1357        );
1358
1359        // Once that frame authenticates (AEAD-opens), the post-decrypt path commits it.
1360        st.confirm_authenticated_source();
1361        assert!(
1362            st.has_migration_candidate(),
1363            "an AEAD-authenticated new source sets the migration candidate"
1364        );
1365    }
1366
1367    /// M-6 (audit 2026-06-11): an ICMP-induced recv error (the forged-RST analogue — a spoofed
1368    /// "port unreachable") must be treated as ADVISORY and retried, never mapped to a fatal
1369    /// `NetworkError` that tears the session down bypassing the liveness machinery (RFC 8085
1370    /// §5.5 / RFC 9000 §14.2). A genuine error stays fatal; a datagram carries its source.
1371    #[test]
1372    fn advisory_icmp_recv_errors_are_retried_not_fatal() {
1373        use std::io::{Error, ErrorKind};
1374        assert!(matches!(
1375            classify_recv(Err(Error::from(ErrorKind::ConnectionRefused))),
1376            RecvAction::Retry
1377        ));
1378        assert!(matches!(
1379            classify_recv(Err(Error::from(ErrorKind::ConnectionReset))),
1380            RecvAction::Retry
1381        ));
1382        assert!(matches!(
1383            classify_recv(Err(Error::from(ErrorKind::PermissionDenied))),
1384            RecvAction::Fatal(_)
1385        ));
1386        let addr: SocketAddr = "127.0.0.1:9".parse().unwrap();
1387        assert!(matches!(
1388            classify_recv(Ok((42, addr))),
1389            RecvAction::Got(42, s) if s == addr
1390        ));
1391    }
1392}