Skip to main content

net/adapter/net/
mod.rs

1//! Net L0 Transport Protocol (Net) adapter.
2//!
3//! Net is a high-performance UDP-based transport protocol designed for
4//! GPU-to-GPU encrypted streaming over UDP. It provides:
5//!
6//! - Zero-copy, zero-allocation hot path
7//! - XChaCha20-Poly1305 encryption per packet
8//! - Noise protocol handshake (NKpsk0)
9//! - Optional per-stream reliability with selective NACKs
10//! - 40-60M events/sec target throughput
11//!
12//! # Usage
13//!
14//! ```rust,ignore
15//! use net::adapter::net::{NetAdapter, NetAdapterConfig, StaticKeypair};
16//!
17//! // Generate keypair for responder
18//! let keypair = StaticKeypair::generate();
19//!
20//! // Create initiator config
21//! let config = NetAdapterConfig::initiator(
22//!     "127.0.0.1:9000".parse()?,
23//!     "127.0.0.1:9001".parse()?,
24//!     psk,
25//!     keypair.public,
26//! );
27//!
28//! // Create adapter
29//! let mut adapter = NetAdapter::new(config)?;
30//! adapter.init().await?;
31//! ```
32
33mod batch;
34pub mod behavior;
35// SDK-level cancel-token registry consumed by the cortex `mesh_rpc`
36// call shapes. Always-built (no cortex feature gate) — the registry
37// is type-pure and small; gating it would mean two parallel
38// definitions, and the `cortex` build is the only consumer today
39// regardless. The `#[allow(dead_code)]` on cortex-off builds keeps
40// the dead-code lint silent without a per-item annotation fan-out.
41#[cfg_attr(not(feature = "cortex"), allow(dead_code))]
42mod cancel_registry;
43pub mod channel;
44pub mod compute;
45mod config;
46pub mod contested;
47pub mod continuity;
48#[cfg(feature = "cortex")]
49pub mod cortex;
50mod crypto;
51mod failure;
52pub mod identity;
53mod mesh;
54// nRPC glue + metrics depend on the cortex fold types (RpcServerFold,
55// RpcClientPending, etc.) and the per-channel-hash inbound dispatcher
56// hook the cortex layer wires up. They're meaningless without
57// `cortex` enabled, and unconditionally exposing them broke `--features
58// net` builds (mesh_rpc.rs references `super::cortex::*`). Gating both
59// keeps the bare-net build clean.
60#[cfg(feature = "dataforts")]
61pub mod dataforts;
62#[cfg(feature = "cortex")]
63pub mod mesh_rpc;
64#[cfg(feature = "cortex")]
65pub mod mesh_rpc_metrics;
66// OA2-E1 §2.4a admission gate glue — bridges the cortex RPC payload
67// (request digest) and the behavior-layer org admission engine.
68// Cortex-gated for the same reason as `mesh_rpc`.
69#[cfg(feature = "netdb")]
70pub mod netdb;
71#[cfg(feature = "cortex")]
72pub mod org_admission_gate;
73mod pool;
74mod protocol;
75mod proxy;
76#[cfg(feature = "redex")]
77pub mod redex;
78mod reliability;
79mod reroute;
80mod route;
81mod router;
82pub mod secret_file;
83mod session;
84pub mod state;
85mod stream;
86pub mod subnet;
87pub mod subprotocol;
88mod swarm;
89mod transport;
90#[cfg(feature = "nat-traversal")]
91pub mod traversal;
92
93#[cfg(target_os = "linux")]
94mod linux;
95
96pub use batch::AdaptiveBatcher;
97pub use channel::{
98    queue_group_hash, AckReason, AclPrincipal, AuthGuard, AuthVerdict, ChannelConfig,
99    ChannelConfigRegistry, ChannelError, ChannelHash, ChannelId, ChannelName, ChannelPublisher,
100    ChannelRegistry, MembershipMsg, OnFailure, OriginBinding, PublishConfig, PublishReport,
101    QueueGroupPolicy, ResolvedConfig, SubscriberRoster, Visibility, SUBPROTOCOL_CHANNEL_MEMBERSHIP,
102};
103pub use compute::{
104    DaemonError, DaemonFactoryRegistry, DaemonHost, DaemonHostConfig, DaemonRegistry, DaemonStats,
105    FactoryEntry, MeshDaemon, MigrationError, MigrationMessage, MigrationOrchestrator,
106    MigrationPhase, MigrationSourceHandler, MigrationState, MigrationTargetHandler,
107    PlacementDecision, Scheduler, SchedulerError, SUBPROTOCOL_MIGRATION,
108};
109pub use config::{ConnectionRole, NetAdapterConfig, ReliabilityConfig};
110pub use contested::{
111    CorrelatedFailureConfig, CorrelatedFailureDetector, CorrelationVerdict, FailureCause,
112    PartitionDetector, PartitionPhase, PartitionRecord, ReconcileOutcome, Side,
113    SUBPROTOCOL_PARTITION,
114};
115pub use continuity::{
116    assess_continuity, CausalCone, Causality, ContinuityProof, ContinuityStatus, Discontinuity,
117    DiscontinuityReason, ForkRecord, HorizonDivergence, ObservationWindow, ProofError,
118    PropagationModel, SuperpositionPhase, SuperpositionState, SUBPROTOCOL_CONTINUITY,
119};
120#[cfg(feature = "cortex")]
121pub use cortex::{
122    CortexAdapter, CortexAdapterConfig, CortexAdapterError, EventEnvelope, EventMeta,
123    FoldErrorPolicy, IntoRedexPayload, StartPosition, EVENT_META_SIZE,
124};
125pub use crypto::{CryptoError, SessionKeys, StaticKeypair};
126pub use failure::{
127    CircuitBreaker, CircuitState, FailureDetector, FailureDetectorConfig, FailureStats,
128    LossSimulator, NodeStatus, PeerFailureEvent, RecoveryAction, RecoveryManager, RecoveryStats,
129    VerdictStatus,
130};
131pub use identity::{
132    EntityError, EntityId, EntityKeypair, OriginStamp, PermissionToken, TokenCache, TokenError,
133    TokenScope,
134};
135/// Exported only so `upgrade_try_acquire_for_test` has a nameable
136/// return type in integration tests; `#[doc(hidden)]` at the definition.
137#[cfg(feature = "nat-traversal")]
138pub use mesh::UpgradeAttemptGuard;
139pub use mesh::{
140    ControlPlaneStats, MeshNode, MeshNodeConfig, PartitionFilter, SensingReadinessOverlay,
141    SensingRegistrationError, UnregisteredChannelPolicy, ACK_RANGES_CAPABILITY_TAG,
142};
143#[cfg(feature = "netdb")]
144pub use netdb::{MemoriesFilter, NetDb, NetDbBuilder, NetDbError, NetDbSnapshot, TasksFilter};
145// `SharedPacketPool` is intentionally not re-exported — see
146// `pool.rs` for the cross-pool nonce-reuse rationale.
147// `PacketPool` itself stays exposed because tests reference it;
148// only the `Arc<PacketPool>` wrapper alias and its constructor
149// are absent.
150pub use pool::{PacketBuilder, PacketPool, SharedLocalPool, ThreadLocalPool};
151pub use protocol::{
152    EventFrame, NackPayload, NetHeader, PacketFlags, HEADER_SIZE, NONCE_SIZE, TAG_SIZE,
153};
154pub use proxy::{
155    ForwardResult, HopStats, MultiHopPacketBuilder, NetProxy, ProxyConfig, ProxyError, ProxyStats,
156};
157#[cfg(feature = "redex")]
158pub use redex::{
159    FsyncPolicy, IndexOp, IndexStart, OrderedAppender, Redex, RedexEntry, RedexError, RedexEvent,
160    RedexFile, RedexFileConfig, RedexFlags, RedexFold, RedexIndex, TypedRedexFile,
161};
162pub use reliability::{FireAndForget, ReliabilityMode, ReliableStream, RetransmitDescriptor};
163pub use reroute::ReroutePolicy;
164pub use route::{
165    AggregateStats, AlternateProvenance, RouteCandidateView, RouteEntry, RouteFlags,
166    RouteObservation, RoutingHeader, RoutingTable, SchedulerStreamStats, TransitionOutcome,
167    ROUTING_HEADER_SIZE,
168};
169pub use router::{FairScheduler, NetRouter, RouteAction, RouterConfig, RouterError, RouterStats};
170// Send-loop drain instrument (NRPC_SEND_LOOP_BATCHING_PLAN). Re-exported only
171// so the in-repo integration tests (which compile as external crates) can
172// drive it; `#[doc(hidden)]` keeps it out of the crate's documented surface.
173#[doc(hidden)]
174pub use router::{
175    arm_send_drain_histo, send_batch_stats, send_drain_histo_snapshot, send_drain_max,
176};
177pub use session::{NetSession, SessionManager, StreamState, TxAdmit, TxSlotGuard};
178pub use state::{
179    CausalChainBuilder, CausalEvent, CausalLink, ChainError, EntityLog, HorizonEncoder, LogError,
180    LogIndex, ObservedHorizon, SnapshotStore, StateSnapshot, CAUSAL_LINK_SIZE, SUBPROTOCOL_CAUSAL,
181    SUBPROTOCOL_SNAPSHOT,
182};
183pub use stream::{
184    CloseBehavior, Reliability, Stream, StreamConfig, StreamError, StreamStats,
185    DEFAULT_STREAM_WINDOW_BYTES,
186};
187pub use subnet::{DropReason, ForwardDecision, SubnetGateway, SubnetId, SubnetPolicy, SubnetRule};
188pub use subprotocol::{
189    negotiate, MigrationHandlerHooks, MigrationOrchestratorPolicy, MigrationSubprotocolHandler,
190    NegotiatedSet, OutboundMigrationMessage, SubprotocolDescriptor, SubprotocolManifest,
191    SubprotocolRegistry, SubprotocolVersion, SUBPROTOCOL_NEGOTIATION,
192};
193pub use swarm::{
194    Capabilities, CapabilityAd, EdgeInfo, GraphStats, LocalGraph, NodeInfo, Pingwave,
195    MAX_GRAPH_NODES, MAX_SEEN_PINGWAVES, PINGWAVE_SIZE,
196};
197pub use transport::{NetSocket, PacketReceiver, PacketSender, ParsedPacket, SocketBufferConfig};
198// Recv-loop batching instrument (NRPC_RECV_LOOP_BATCHING_PLAN), symmetric to
199// the send-side drain instrument. Compiled only under the `batched-ingress`
200// build feature (it measures that path). Re-exported only so the in-repo
201// integration tests can drive it; `#[doc(hidden)]` keeps it off the
202// documented surface.
203#[cfg(feature = "batched-ingress")]
204#[doc(hidden)]
205pub use transport::{
206    arm_recv_drain_histo, recv_batch_stats, recv_drain_histo_snapshot, recv_drain_max,
207    RECV_DRAIN_BUCKETS,
208};
209
210use async_trait::async_trait;
211use bytes::Bytes;
212use crossbeam_queue::SegQueue;
213use dashmap::DashMap;
214use std::sync::atomic::{AtomicBool, Ordering};
215use std::sync::Arc;
216use tokio::sync::Mutex as TokioMutex;
217use tokio::sync::Notify;
218use tokio::task::JoinHandle;
219
220use crate::adapter::{Adapter, ShardPollResult};
221use crate::error::AdapterError;
222use crate::event::{Batch, StoredEvent};
223
224use crypto::NoiseHandshake;
225use session::SessionManager as SessionMgr;
226use transport::NetSocket as Socket;
227
228// Re-export xxh3 utilities for stream routing
229pub use routing::{route_to_shard, stream_id_from_bytes, stream_id_from_key};
230
231/// Threshold below which the cached coarse-clock reading is reused
232/// instead of re-reading the OS wall clock. 1 ms is well below the
233/// session-timeout / heartbeat / NACK cadence the consumers care
234/// about (those tick on the seconds scale), and well above the
235/// `Instant::now` cost (~10 ns) we still pay per call to gate the
236/// cache. Per PERF_AUDIT §2.7.
237const COARSE_CLOCK_REFRESH_NS: u64 = 1_000_000; // 1 ms
238
239/// Current timestamp in nanoseconds since the Unix epoch.
240///
241/// Shared utility — avoids duplicating this across `causal.rs`, `snapshot.rs`,
242/// `observation.rs`, `migration.rs`, `session.rs`, and `token.rs`.
243///
244/// Saturates via `try_from` so future-dated clocks land at
245/// `u64::MAX` instead of wrapping near 0. A bare `as u64` would
246/// silently truncate the `u128` returned by
247/// `Duration::as_nanos()`. Practical wraparound from monotonic
248/// flow doesn't happen until ~year 2554, but a system whose clock
249/// was misconfigured to a far-future date would produce a tiny
250/// truncated timestamp — immediately tripping `is_timed_out`
251/// everywhere. `unwrap_or_default()` returning `Duration::ZERO`
252/// for a pre-epoch clock would also produce identical timestamps
253/// that break ordering.
254///
255/// Coarse-clock cached per thread at [`COARSE_CLOCK_REFRESH_NS`]
256/// granularity (PERF_AUDIT §2.7) — readings may be up to 1 ms
257/// stale, and two threads may disagree by up to that much.
258/// Consumers doing timeout arithmetic MUST use `saturating_sub`
259/// (they all do today) so a reader with a staler cache than the
260/// toucher can't wrap and false-expire.
261#[inline]
262pub(crate) fn current_timestamp() -> u64 {
263    // **PERF_AUDIT §2.7.** Per-packet RX/TX paths each call
264    // `current_timestamp()` twice (one stream `touch` + one session
265    // `touch`). On Windows `SystemTime::now()` is
266    // `GetSystemTimePreciseAsFileTime` (~600 ns); on Linux it's
267    // `clock_gettime(CLOCK_REALTIME)` (~120 ns). At sustained packet
268    // rates the four wall-clock reads per ping eat measurable CPU.
269    //
270    // Coarse-clock cache: a `thread_local!` Cell holds the last
271    // `(Instant, u64-ns)` pair. Each call asks `Instant::now()`
272    // (~10 ns — TSC-backed on both Linux and Windows) whether 1 ms
273    // has elapsed; if not, the cached `u64` is reused. Repeated
274    // calls within the same millisecond from the same thread pay
275    // one Instant comparison instead of one OS wall-clock syscall.
276    //
277    // Consumers (`session.is_timed_out` against multi-second
278    // timeouts, `last_activity_ns` for diagnostics) are insensitive
279    // to ≤ 1 ms drift; the wire envelopes that need absolute epoch
280    // ns (capability announcements, snapshots) call
281    // `current_timestamp_micros` or stamp `SystemTime::now()`
282    // directly — neither hits this path.
283    thread_local! {
284        static COARSE_CLOCK: std::cell::Cell<Option<(std::time::Instant, u64)>>
285            = const { std::cell::Cell::new(None) };
286    }
287    COARSE_CLOCK.with(|cell| {
288        let now_inst = std::time::Instant::now();
289        let (store, ns) = coarse_clock_advance(cell.get(), now_inst, || {
290            let elapsed = std::time::SystemTime::now()
291                .duration_since(std::time::UNIX_EPOCH)
292                .unwrap_or_default();
293            u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX)
294        });
295        if let Some(pair) = store {
296            cell.set(Some(pair));
297        }
298        ns
299    })
300}
301
302/// Pure core of the §2.7 coarse clock: given the cached
303/// `(instant, ns)` pair and the current `Instant`, decide whether
304/// to reuse the cached reading (younger than
305/// [`COARSE_CLOCK_REFRESH_NS`]) or call `read_wall` for a fresh
306/// wall-clock value. Returns `(cache update, value)` — `None`
307/// means a cache hit (nothing to store, keeping the hit path
308/// store-free); `Some(pair)` rebases the refresh window on the
309/// read instant.
310///
311/// Extracted from [`current_timestamp`] so the reuse/refresh
312/// decision is testable with synthetic instants. The previous
313/// test drove the real thread-local with a 100-read burst and
314/// asserted every value matched — correct on an idle machine, but
315/// a > 1 ms OS preemption mid-burst legitimately rolls the window,
316/// so the assertion was probabilistic under CI load even with
317/// retries.
318#[inline]
319fn coarse_clock_advance(
320    cached: Option<(std::time::Instant, u64)>,
321    now_inst: std::time::Instant,
322    read_wall: impl FnOnce() -> u64,
323) -> (Option<(std::time::Instant, u64)>, u64) {
324    if let Some((last_inst, last_ns)) = cached {
325        if now_inst.duration_since(last_inst).as_nanos() < COARSE_CLOCK_REFRESH_NS as u128 {
326            return (None, last_ns);
327        }
328    }
329    let ns = read_wall();
330    (Some((now_inst, ns)), ns)
331}
332
333/// Current timestamp in microseconds since the Unix epoch.
334/// Saturates at `0` on pre-epoch clocks (the wire envelopes that
335/// consume this — fold announcements, snapshots — treat micros
336/// purely as diagnostics, never as ordering, so a saturated
337/// reading is benign).
338#[inline]
339pub(crate) fn current_timestamp_micros() -> u64 {
340    std::time::SystemTime::now()
341        .duration_since(std::time::UNIX_EPOCH)
342        .map(|d| d.as_micros() as u64)
343        .unwrap_or(0)
344}
345
346/// Fast xxh3-based routing utilities for Net streams.
347///
348/// Uses xxh3 (~50GB/s) for deterministic, high-performance stream routing.
349mod routing {
350    use xxhash_rust::xxh3::xxh3_64;
351
352    /// Generate a stream ID from arbitrary data.
353    ///
354    /// Uses xxh3 for fast, deterministic hashing (~50GB/s on modern CPUs).
355    #[inline]
356    pub fn stream_id_from_bytes(data: &[u8]) -> u64 {
357        xxh3_64(data)
358    }
359
360    /// Generate a stream ID from a string key.
361    ///
362    /// Convenience wrapper for `stream_id_from_bytes`.
363    #[inline]
364    pub fn stream_id_from_key(key: &str) -> u64 {
365        xxh3_64(key.as_bytes())
366    }
367
368    /// Route data to a shard based on its content hash.
369    ///
370    /// Returns a shard ID in the range `[0, num_shards)`.
371    ///
372    /// # Panics
373    ///
374    /// Panics if `num_shards` is 0.
375    #[inline]
376    pub fn route_to_shard(data: &[u8], num_shards: u16) -> u16 {
377        assert!(num_shards > 0, "num_shards must be > 0");
378        (xxh3_64(data) % num_shards as u64) as u16
379    }
380
381    #[cfg(test)]
382    mod tests {
383        use super::*;
384
385        #[test]
386        fn test_stream_id_deterministic() {
387            let data = b"test event data";
388            let id1 = stream_id_from_bytes(data);
389            let id2 = stream_id_from_bytes(data);
390            assert_eq!(id1, id2);
391        }
392
393        #[test]
394        fn test_stream_id_different_for_different_data() {
395            let id1 = stream_id_from_bytes(b"event1");
396            let id2 = stream_id_from_bytes(b"event2");
397            assert_ne!(id1, id2);
398        }
399
400        #[test]
401        fn test_stream_id_from_key() {
402            let id = stream_id_from_key("user:12345");
403            assert_ne!(id, 0);
404        }
405
406        #[test]
407        fn test_route_to_shard_range() {
408            let num_shards = 16u16;
409            for i in 0..1000 {
410                let data = format!("event_{}", i);
411                let shard = route_to_shard(data.as_bytes(), num_shards);
412                assert!(shard < num_shards);
413            }
414        }
415
416        #[test]
417        #[should_panic(expected = "num_shards must be > 0")]
418        fn test_route_to_shard_zero_shards_panics() {
419            // Regression: route_to_shard(_, 0) caused a divide-by-zero panic
420            // with no helpful message. Now it asserts with a clear message.
421            route_to_shard(b"test", 0);
422        }
423
424        #[test]
425        fn test_route_to_shard_distribution() {
426            let num_shards = 8u16;
427            let mut counts = [0u32; 8];
428
429            for i in 0..8000 {
430                let data = format!("event_{}", i);
431                let shard = route_to_shard(data.as_bytes(), num_shards);
432                counts[shard as usize] += 1;
433            }
434
435            // Check that distribution is reasonably uniform (within 50% of expected)
436            let expected = 1000;
437            for count in counts {
438                assert!(count > expected / 2, "shard count {} too low", count);
439                assert!(count < expected * 2, "shard count {} too high", count);
440            }
441        }
442    }
443}
444
445/// Shared inbound queue type
446type InboundQueues = Arc<DashMap<u16, SegQueue<StoredEvent>>>;
447
448/// Per-source rate limiter for the handshake responder loop.
449///
450/// The responder used to accept whichever source emitted msg1
451/// first, with no per-source pacing — an attacker who knows the PSK
452/// (PSKs are typically multi-tenant) could race the legitimate
453/// initiator's msg1; even without the PSK an attacker could flood
454/// handshake-flagged datagrams to monopolize the recv loop.
455///
456/// `HandshakePacer` keeps a rolling count of recent attempts per
457/// source and rejects sources that exceed the budget within the
458/// window. Expired entries are garbage-collected on a periodic
459/// schedule rather than on every check, so a sustained flood from
460/// many distinct sources doesn't pay an O(n) sweep per packet.
461pub(crate) struct HandshakePacer {
462    /// Per-source `(count_in_window, window_start)`.
463    entries: std::collections::HashMap<std::net::SocketAddr, (u32, std::time::Instant)>,
464    /// Maximum attempts per source within `window`.
465    max_per_window: u32,
466    /// Window length.
467    window: std::time::Duration,
468    /// Last time we ran the GC pass.
469    last_gc: std::time::Instant,
470    /// Soft cap on `entries` size before forcing a GC pass even
471    /// before the periodic deadline. Keeps memory bounded against
472    /// an attacker fanning across many spoofed source addresses.
473    gc_size_threshold: usize,
474}
475
476impl HandshakePacer {
477    pub(crate) fn new(max_per_window: u32, window: std::time::Duration) -> Self {
478        Self {
479            entries: std::collections::HashMap::new(),
480            max_per_window,
481            window,
482            last_gc: std::time::Instant::now(),
483            // 4096 entries × ~40 bytes each ≈ 160 KiB — comfortable
484            // ceiling that still triggers GC well before any
485            // realistic memory issue.
486            gc_size_threshold: 4096,
487        }
488    }
489
490    /// Record an attempt from `source`. Returns `true` if the source
491    /// is within budget (caller may proceed); `false` if it has
492    /// exceeded the rate limit (caller must drop the packet).
493    pub(crate) fn check_and_record(&mut self, source: std::net::SocketAddr) -> bool {
494        let now = std::time::Instant::now();
495        // Amortized GC: only run the O(n) `retain` sweep when one
496        // of two thresholds trips:
497        //   1. We haven't GC'd in `window` (entries are valid for
498        //      at most `2 * window` so a once-per-`window` cadence
499        //      is sufficient to keep the map proportional to the
500        //      active source set).
501        //   2. The map exceeds `gc_size_threshold`, indicating a
502        //      flood attempt across many spoofed sources.
503        if now.duration_since(self.last_gc) >= self.window
504            || self.entries.len() >= self.gc_size_threshold
505        {
506            let cutoff = self.window.saturating_mul(2);
507            self.entries
508                .retain(|_, (_, start)| now.duration_since(*start) < cutoff);
509            self.last_gc = now;
510        }
511
512        let entry = self.entries.entry(source).or_insert((0, now));
513        if now.duration_since(entry.1) > self.window {
514            // Window expired; reset the counter.
515            entry.0 = 0;
516            entry.1 = now;
517        }
518        entry.0 = entry.0.saturating_add(1);
519        entry.0 <= self.max_per_window
520    }
521}
522
523/// Net adapter for high-performance UDP transport.
524pub struct NetAdapter {
525    /// Configuration
526    config: NetAdapterConfig,
527    /// UDP socket
528    socket: Option<Arc<Socket>>,
529    /// Session (stored separately for init)
530    session: Option<Arc<NetSession>>,
531    /// Session manager
532    session_manager: SessionMgr,
533    /// Inbound events per shard (for poll_shard)
534    inbound: InboundQueues,
535    /// Background tasks
536    tasks: TokioMutex<Vec<JoinHandle<()>>>,
537    /// Shutdown signal (flag for polling, Notify for waking blocked tasks)
538    shutdown: Arc<AtomicBool>,
539    /// Notify to wake tasks blocked on I/O during shutdown
540    shutdown_notify: Arc<Notify>,
541    /// Initialization state
542    initialized: AtomicBool,
543    /// Per-source rate limiter for the handshake responder loop.
544    /// Without this, an attacker can flood handshake-flagged
545    /// datagrams to monopolize the recv path or race a legitimate
546    /// initiator's msg1.
547    handshake_pacer: parking_lot::Mutex<HandshakePacer>,
548}
549
550impl NetAdapter {
551    /// Create a new Net adapter.
552    pub fn new(config: NetAdapterConfig) -> Result<Self, AdapterError> {
553        config
554            .validate()
555            .map_err(|e| AdapterError::Fatal(format!("invalid config: {}", e)))?;
556
557        Ok(Self {
558            session_manager: SessionMgr::new(config.session_timeout),
559            config,
560            socket: None,
561            session: None,
562            inbound: Arc::new(DashMap::new()),
563            tasks: TokioMutex::new(Vec::new()),
564            shutdown: Arc::new(AtomicBool::new(false)),
565            shutdown_notify: Arc::new(Notify::new()),
566            initialized: AtomicBool::new(false),
567            // 5 attempts per second per source, plenty for any
568            // legitimate initiator (RTT-limited) and tight enough
569            // to throttle a flooder on consumer-grade hardware.
570            handshake_pacer: parking_lot::Mutex::new(HandshakePacer::new(
571                5,
572                std::time::Duration::from_secs(1),
573            )),
574        })
575    }
576
577    /// Perform Noise handshake with peer.
578    /// Returns session keys and the actual peer address (from the wire, not config).
579    async fn perform_handshake(
580        &self,
581        socket: &Socket,
582    ) -> Result<(SessionKeys, std::net::SocketAddr), AdapterError> {
583        let mut attempt = 0;
584        let max_attempts = self.config.handshake_retries;
585
586        // Cap per-attempt sleep so a misconfigured `handshake_retries`
587        // near `MAX_HANDSHAKE_RETRIES` (1024) cannot pin `init()` for
588        // hours. Pre-fix `100 * attempt` grew linearly and unbounded:
589        // attempt 1024 slept ~102s, with cumulative wait across all
590        // attempts approaching 14 hours. Capping at 5s gives bounded
591        // worst-case `max_attempts × 5s` (~85 minutes at the cap),
592        // which is still long but not unbounded.
593        const HANDSHAKE_RETRY_SLEEP_CAP_MS: u64 = 5_000;
594
595        loop {
596            attempt += 1;
597            match self.try_handshake(socket).await {
598                Ok(result) => return Ok(result),
599                Err(e) if attempt < max_attempts => {
600                    tracing::warn!(
601                        attempt = attempt,
602                        max = max_attempts,
603                        error = %e,
604                        "handshake failed, retrying"
605                    );
606                    let backoff_ms =
607                        (100u64.saturating_mul(attempt as u64)).min(HANDSHAKE_RETRY_SLEEP_CAP_MS);
608                    tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
609                }
610                Err(e) => return Err(e),
611            }
612        }
613    }
614
615    /// Single handshake attempt.
616    /// Returns session keys and the actual peer address.
617    async fn try_handshake(
618        &self,
619        socket: &Socket,
620    ) -> Result<(SessionKeys, std::net::SocketAddr), AdapterError> {
621        let timeout = self.config.handshake_timeout;
622        let socket_arc = socket.socket_arc();
623
624        if self.config.is_initiator() {
625            // Initiator flow
626            let peer_pubkey = self
627                .config
628                .peer_static_pubkey
629                .as_ref()
630                .ok_or_else(|| AdapterError::Fatal("missing peer public key".into()))?;
631
632            let mut handshake = NoiseHandshake::initiator(&self.config.psk, peer_pubkey)
633                .map_err(|e| AdapterError::Fatal(format!("handshake init failed: {}", e)))?;
634
635            // Send first message
636            let msg1 = handshake
637                .write_message(&[])
638                .map_err(|e| AdapterError::Connection(format!("write_message failed: {}", e)))?;
639
640            let mut builder = PacketBuilder::new(&[0u8; 32], 0);
641            let packet = builder.build_handshake(&msg1);
642
643            socket
644                .send_to(&packet, self.config.peer_addr)
645                .await
646                .map_err(|e| AdapterError::Connection(format!("send failed: {}", e)))?;
647
648            // Receive response, discarding datagrams that are not handshake
649            // packets from the expected peer. This prevents stray traffic on
650            // the shared socket from consuming the handshake slot.
651            let (parsed, _source) = tokio::time::timeout(timeout, async {
652                // Stack buffer reused across loop iterations.
653                // `MAX_PACKET_SIZE` is 8192 bytes — small enough to
654                // live on the async stack without spilling, and the
655                // reuse drops the per-iteration `BytesMut::with_capacity`
656                // alloc on the stray-traffic path. Pre-fix every
657                // discarded datagram (an off-peer packet, an invalid
658                // handshake) allocated a fresh 8 KiB and freed it at
659                // loop end — under stray UDP traffic on the same
660                // bind port this churned the allocator. Only the
661                // success path now allocates (a `Bytes::copy_from_slice`
662                // sized to the actual payload, since `ParsedPacket`
663                // owns its `Bytes`).
664                let mut recv_buf = [0u8; protocol::MAX_PACKET_SIZE];
665                loop {
666                    let (n, source) = socket_arc
667                        .recv_from(&mut recv_buf)
668                        .await
669                        .map_err(|e| AdapterError::Connection(format!("recv failed: {}", e)))?;
670
671                    // Only accept packets from the peer we initiated with
672                    if source != self.config.peer_addr {
673                        continue;
674                    }
675
676                    let data = bytes::Bytes::copy_from_slice(&recv_buf[..n]);
677
678                    if let Some(p) = ParsedPacket::parse(data, source) {
679                        if p.header.flags.is_handshake() {
680                            return Ok::<_, AdapterError>((p, source));
681                        }
682                    }
683                    // Not a valid handshake packet from our peer — keep waiting
684                }
685            })
686            .await
687            .map_err(|_| AdapterError::Connection("handshake timeout".into()))??;
688
689            // Process response
690            handshake
691                .read_message(&parsed.payload)
692                .map_err(|e| AdapterError::Connection(format!("read_message failed: {}", e)))?;
693
694            // Extract session keys
695            let keys = handshake
696                .into_session_keys()
697                .map_err(|e| AdapterError::Fatal(format!("key extraction failed: {}", e)))?;
698            Ok((keys, self.config.peer_addr))
699        } else {
700            // Responder flow
701            let keypair = self
702                .config
703                .static_keypair
704                .as_ref()
705                .ok_or_else(|| AdapterError::Fatal("missing static keypair".into()))?;
706
707            // Wait for an initiator handshake message, discarding any
708            // non-handshake datagrams that arrive on the shared
709            // socket. Per-source pacing throttles flooders so the
710            // legitimate initiator's msg1 can land — without it,
711            // an attacker could blast handshake-flagged datagrams
712            // and monopolize this recv loop.
713            let (parsed, source) = tokio::time::timeout(timeout, async {
714                loop {
715                    let mut recv_buf = bytes::BytesMut::with_capacity(protocol::MAX_PACKET_SIZE);
716                    recv_buf.resize(protocol::MAX_PACKET_SIZE, 0);
717
718                    let (n, source) = socket_arc
719                        .recv_from(&mut recv_buf)
720                        .await
721                        .map_err(|e| AdapterError::Connection(format!("recv failed: {}", e)))?;
722
723                    recv_buf.truncate(n);
724                    let data = recv_buf.freeze();
725
726                    if let Some(p) = ParsedPacket::parse(data, source) {
727                        if p.header.flags.is_handshake() {
728                            // Per-source pacing: drop packets from
729                            // sources that exceed the budget.
730                            let allowed = self.handshake_pacer.lock().check_and_record(source);
731                            if !allowed {
732                                tracing::debug!(
733                                    %source,
734                                    "handshake responder: dropping packet from \
735                                     rate-limited source"
736                                );
737                                continue;
738                            }
739                            return Ok::<_, AdapterError>((p, source));
740                        }
741                    }
742                    // Not a valid handshake packet — keep waiting
743                }
744            })
745            .await
746            .map_err(|_| AdapterError::Connection("handshake timeout".into()))??;
747
748            let mut handshake = NoiseHandshake::responder(&self.config.psk, keypair)
749                .map_err(|e| AdapterError::Fatal(format!("handshake init failed: {}", e)))?;
750
751            // Process initiator message
752            handshake
753                .read_message(&parsed.payload)
754                .map_err(|e| AdapterError::Connection(format!("read_message failed: {}", e)))?;
755
756            // Send response
757            let msg2 = handshake
758                .write_message(&[])
759                .map_err(|e| AdapterError::Connection(format!("write_message failed: {}", e)))?;
760
761            let mut builder = PacketBuilder::new(&[0u8; 32], 0);
762            let packet = builder.build_handshake(&msg2);
763
764            // Reply to the actual source address (not the configured peer_addr),
765            // so the handshake completes even behind NAT or when the config is stale.
766            socket
767                .send_to(&packet, source)
768                .await
769                .map_err(|e| AdapterError::Connection(format!("send failed: {}", e)))?;
770
771            // Extract session keys and use the actual source address as peer
772            let keys = handshake
773                .into_session_keys()
774                .map_err(|e| AdapterError::Fatal(format!("key extraction failed: {}", e)))?;
775            Ok((keys, source))
776        }
777    }
778
779    /// Process a single received packet: parse, decrypt, and queue events.
780    fn process_packet(
781        data: Bytes,
782        source: std::net::SocketAddr,
783        session: &NetSession,
784        inbound: &InboundQueues,
785        num_shards: u16,
786    ) {
787        // Parse packet
788        let mut parsed = match ParsedPacket::parse(data, source) {
789            Some(p) => p,
790            None => return,
791        };
792
793        // Reject packets whose actual payload size doesn't match the declared
794        // length. This catches truncated or oversized packets before they
795        // reach the decrypt path.
796        if !parsed.header.flags.is_handshake()
797            && !parsed.header.flags.is_heartbeat()
798            && !parsed.is_valid_length()
799        {
800            return;
801        }
802
803        // Skip handshake packets in the data path (handled during init)
804        if parsed.header.flags.is_handshake() {
805            return;
806        }
807
808        // Validate session before any state mutation (including touch)
809        if parsed.header.session_id != session.session_id() {
810            return;
811        }
812
813        // Heartbeats are AEAD-tagged: the empty payload encrypts to
814        // a 16-byte Poly1305 tag, and the receiver verifies the
815        // tag here. Without this check, an off-path attacker who
816        // observed or guessed the session_id could spoof
817        // heartbeats and keep a session alive (the source-address
818        // check on UDP is itself spoofable, and session_id is in
819        // cleartext on every prior packet).
820        //
821        // We still require `source == peer_addr` as a cheap
822        // first-line filter so an unauthenticated flood doesn't
823        // get to do the AEAD verify.
824        //
825        // The verify+touch sequence lives inside
826        // `NetSession::verify_and_touch_heartbeat` so callers can't
827        // touch a session whose heartbeat failed verify, and can't
828        // forget to touch on success.
829        if parsed.header.flags.is_heartbeat() {
830            if source == session.peer_addr() {
831                session.verify_and_touch_heartbeat(&parsed);
832            }
833            return;
834        }
835
836        // Decrypt payload. Per crypto-session perf #128, route
837        // through `decrypt_to_bytes` so the common case
838        // (refcount-1 inbound buffer) decrypts in place instead
839        // of allocating a fresh `Vec<u8>` plaintext per packet.
840        let aad = parsed.header.aad();
841        let counter = u64::from_le_bytes(parsed.header.nonce[4..12].try_into().unwrap_or([0u8; 8]));
842        let rx_cipher = session.rx_cipher();
843        let payload = std::mem::take(&mut parsed.payload);
844        // Per crypto-session perf #132: collapsed the pre-decrypt
845        // `is_valid_rx_counter` + post-decrypt `update_rx_counter`
846        // two-step into one `try_admit_rx_counter` call. See
847        // `mesh.rs::process_local_packet` for the full rationale —
848        // the contract is identical (replays still rejected, TOCTOU
849        // still closed), and the redundant Mutex lock on every
850        // non-replay packet is gone.
851        let decrypted = match rx_cipher.decrypt_to_bytes(counter, &aad, payload) {
852            Ok(d) => {
853                if !rx_cipher.try_admit_rx_counter(counter) {
854                    return;
855                }
856                d
857            }
858            Err(_) => return,
859        };
860
861        // Parse events
862        let events = EventFrame::read_events(decrypted, parsed.header.event_count);
863
864        // Update stream state
865        let stream_id = parsed.header.stream_id;
866        let shard_id = if num_shards > 0 {
867            (stream_id % num_shards as u64) as u16
868        } else {
869            0
870        };
871
872        // Previously the boolean result of `r.on_receive(seq)` was
873        // discarded — a duplicate (NACK retransmit, rebroadcast,
874        // etc.) returned `false` but the events were still queued for
875        // poll_shard, breaking exactly-once delivery on reliable
876        // streams. The cipher's replay window doesn't catch this
877        // because each retransmit is re-encrypted with a fresh outer
878        // counter.
879        //
880        // Now: if `on_receive` reports a duplicate, we still call
881        // `session.touch()` (the peer is alive) but skip the queue
882        // step entirely — the original delivery already queued the
883        // events.
884        let is_fresh = {
885            let stream = session.get_or_create_stream(stream_id);
886            // `with_reliability` always invokes the closure (it
887            // locks an internal `Mutex<Box<dyn ReliabilityMode>>`).
888            // For streams without a meaningful reliability mode the
889            // implementation returns `true` for every `on_receive`,
890            // matching the historical "always queue" behavior.
891            let fresh = stream.with_reliability(|r| r.on_receive(parsed.header.sequence));
892            stream.update_rx_seq(parsed.header.sequence);
893            fresh
894        };
895
896        if is_fresh {
897            // Queue events for poll_shard
898            let queue = inbound.entry(shard_id).or_default();
899            let seq = parsed.header.sequence;
900            for (i, event_data) in events.into_iter().enumerate() {
901                use std::fmt::Write;
902                let mut event_id = String::with_capacity(24);
903                let _ = write!(event_id, "{}:{}", seq, i);
904                queue.push(StoredEvent::new(event_id, event_data, seq, shard_id));
905            }
906        } else {
907            tracing::debug!(
908                seq = parsed.header.sequence,
909                stream_id,
910                "Dropping duplicate packet"
911            );
912        }
913
914        session.touch();
915    }
916
917    /// Spawn receiver task.
918    ///
919    /// On Linux, uses a dedicated OS thread with batched recvmmsg for up to
920    /// 64 packets per syscall. On other platforms, uses standard async recv.
921    ///
922    /// Note: `BatchedPacketReceiver`'s channel carries a whole recvmmsg batch
923    /// per message (depth measured in batches, not packets). That is a shared
924    /// property of the type — see its docstring — and applies here regardless
925    /// of the `batched-ingress` feature, which only gates the mesh opt-in and
926    /// the measurement instrument, not this always-on adapter path.
927    #[cfg(target_os = "linux")]
928    fn spawn_receiver(
929        shutdown: Arc<AtomicBool>,
930        shutdown_notify: Arc<Notify>,
931        socket: Arc<Socket>,
932        session: Arc<NetSession>,
933        inbound: InboundQueues,
934        num_shards: u16,
935    ) -> JoinHandle<()> {
936        let mut receiver = transport::BatchedPacketReceiver::new(socket.socket_arc());
937
938        tokio::spawn(async move {
939            while !shutdown.load(Ordering::Acquire) {
940                tokio::select! {
941                    result = receiver.recv() => {
942                        match result {
943                            Ok((data, source)) => {
944                                Self::process_packet(data, source, &session, &inbound, num_shards);
945                            }
946                            Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset => {
947                                tracing::warn!("batch receiver thread exited, stopping receiver");
948                                break;
949                            }
950                            Err(e) => {
951                                if !shutdown.load(Ordering::Acquire) {
952                                    tracing::warn!(error = %e, "receive error");
953                                }
954                            }
955                        }
956                    }
957                    _ = shutdown_notify.notified() => {
958                        break;
959                    }
960                }
961            }
962        })
963    }
964
965    /// Spawn receiver task (non-Linux fallback).
966    #[cfg(not(target_os = "linux"))]
967    fn spawn_receiver(
968        shutdown: Arc<AtomicBool>,
969        shutdown_notify: Arc<Notify>,
970        socket: Arc<Socket>,
971        session: Arc<NetSession>,
972        inbound: InboundQueues,
973        num_shards: u16,
974    ) -> JoinHandle<()> {
975        tokio::spawn(async move {
976            let mut receiver = PacketReceiver::new(socket.socket_arc());
977
978            while !shutdown.load(Ordering::Acquire) {
979                // Race recv against shutdown notification so the task
980                // can exit promptly instead of blocking on recv_from
981                // until a packet arrives.
982                tokio::select! {
983                    result = receiver.recv() => {
984                        match result {
985                            Ok((data, source)) => {
986                                Self::process_packet(data, source, &session, &inbound, num_shards);
987                            }
988                            Err(e) => {
989                                if !shutdown.load(Ordering::Acquire) {
990                                    tracing::warn!(error = %e, "receive error");
991                                }
992                            }
993                        }
994                    }
995                    _ = shutdown_notify.notified() => {
996                        break;
997                    }
998                }
999            }
1000        })
1001    }
1002
1003    /// Spawn heartbeat task.
1004    fn spawn_heartbeat(
1005        shutdown: Arc<AtomicBool>,
1006        shutdown_notify: Arc<Notify>,
1007        socket: Arc<Socket>,
1008        session: Arc<NetSession>,
1009        interval: std::time::Duration,
1010        peer_addr: std::net::SocketAddr,
1011    ) -> JoinHandle<()> {
1012        tokio::spawn(async move {
1013            let mut ticker = tokio::time::interval(interval);
1014
1015            loop {
1016                tokio::select! {
1017                    _ = ticker.tick() => {
1018                        if shutdown.load(Ordering::Acquire) || !session.is_active() {
1019                            break;
1020                        }
1021
1022                        // `Session::build_heartbeat` routes through
1023                        // `thread_local_pool` (same pool the data
1024                        // path uses) so heartbeats share a single
1025                        // TX counter with data and interleave
1026                        // correctly on the wire. Constructing a
1027                        // bespoke `PacketBuilder::new(&[0u8; 32],
1028                        // session.session_id())` per tick would
1029                        // (a) use the wrong key so the receiver's
1030                        // AEAD verify would reject every heartbeat,
1031                        // and (b) reuse counter=0 across successive
1032                        // heartbeats so the receiver's replay
1033                        // window would reject every heartbeat
1034                        // after the first.
1035                        let packet = session.build_heartbeat();
1036
1037                        if let Err(e) = socket.send_to(&packet, peer_addr).await {
1038                            tracing::warn!(error = %e, "heartbeat send failed");
1039                        }
1040                    }
1041                    _ = shutdown_notify.notified() => {
1042                        break;
1043                    }
1044                }
1045            }
1046        })
1047    }
1048}
1049
1050#[async_trait]
1051impl Adapter for NetAdapter {
1052    async fn init(&mut self) -> Result<(), AdapterError> {
1053        if self.initialized.load(Ordering::Acquire) {
1054            return Ok(());
1055        }
1056
1057        // Create socket with configured buffer sizes
1058        let socket_config = match (
1059            self.config.socket_recv_buffer,
1060            self.config.socket_send_buffer,
1061        ) {
1062            (Some(recv), Some(send)) => transport::SocketBufferConfig {
1063                recv_buffer_size: recv,
1064                send_buffer_size: send,
1065            },
1066            _ => transport::SocketBufferConfig::default(),
1067        };
1068        let socket = Socket::with_config(self.config.bind_addr, socket_config)
1069            .await
1070            .map_err(|e| AdapterError::Connection(format!("socket creation failed: {}", e)))?;
1071
1072        let socket = Arc::new(socket);
1073        self.socket = Some(socket.clone());
1074
1075        // Perform handshake — actual_peer is the real address from the wire
1076        let (keys, actual_peer) = self.perform_handshake(&socket).await?;
1077
1078        // Create packet pool with TX key
1079        // Create session with the actual peer address (not the configured one,
1080        // which may be stale or pre-NAT)
1081        let session = Arc::new(NetSession::new(
1082            keys,
1083            actual_peer,
1084            self.config.packet_pool_size,
1085            self.config.default_reliability.is_reliable(),
1086        ));
1087        self.session = Some(session.clone());
1088
1089        // Store in session manager for health checks (same Arc as the active session)
1090        self.session_manager.set_session_arc(session.clone());
1091
1092        // Spawn background tasks
1093        let recv_task = Self::spawn_receiver(
1094            self.shutdown.clone(),
1095            self.shutdown_notify.clone(),
1096            socket.clone(),
1097            session.clone(),
1098            self.inbound.clone(),
1099            self.config.num_shards,
1100        );
1101
1102        let heartbeat_task = Self::spawn_heartbeat(
1103            self.shutdown.clone(),
1104            self.shutdown_notify.clone(),
1105            socket,
1106            session,
1107            self.config.heartbeat_interval,
1108            actual_peer,
1109        );
1110
1111        {
1112            let mut tasks = self.tasks.lock().await;
1113            tasks.push(recv_task);
1114            tasks.push(heartbeat_task);
1115        }
1116
1117        self.initialized.store(true, Ordering::Release);
1118
1119        tracing::info!(
1120            bind_addr = %self.config.bind_addr,
1121            peer_addr = %self.config.peer_addr,
1122            role = ?self.config.role,
1123            "Net adapter initialized"
1124        );
1125
1126        Ok(())
1127    }
1128
1129    async fn on_batch(&self, batch: std::sync::Arc<Batch>) -> Result<(), AdapterError> {
1130        let session = self
1131            .session
1132            .as_ref()
1133            .ok_or_else(|| AdapterError::Connection("not connected".into()))?;
1134
1135        let socket = self
1136            .socket
1137            .as_ref()
1138            .ok_or_else(|| AdapterError::Connection("socket not initialized".into()))?;
1139
1140        let stream_id = batch.shard_id as u64;
1141        let peer_addr = session.peer_addr();
1142
1143        // Read stream config under the lock, then drop it immediately.
1144        // Holding the DashMap RefMut across .await would deadlock against
1145        // the receiver task which also calls get_or_create_stream().
1146        let reliable = {
1147            let stream = session.get_or_create_stream(stream_id);
1148            stream.with_reliability(|r| r.needs_ack())
1149            // RefMut dropped here
1150        };
1151
1152        // Convert events to bytes and batch them
1153        let mut current_batch: Vec<Bytes> = Vec::with_capacity(64);
1154        let mut current_size = 0usize;
1155
1156        // Thread-local pool with counter-based nonces — zero contention
1157        let pool = session.thread_local_pool();
1158        let mut builder = pool.get();
1159
1160        for event in &batch.events {
1161            let event_bytes = event.raw.clone();
1162            let frame_size = EventFrame::LEN_SIZE + event_bytes.len();
1163
1164            // Check if adding this event would exceed packet size
1165            if current_size + frame_size > protocol::MAX_PAYLOAD_SIZE && !current_batch.is_empty() {
1166                // Acquire stream lock briefly for seq + reliability tracking
1167                let seq;
1168                {
1169                    let stream = session.get_or_create_stream(stream_id);
1170                    seq = stream.next_tx_seq();
1171                }
1172
1173                let flags = if reliable {
1174                    PacketFlags::RELIABLE
1175                } else {
1176                    PacketFlags::NONE
1177                };
1178
1179                let packet = builder.build(stream_id, seq, &current_batch, flags);
1180
1181                // No DashMap lock held during this .await
1182                socket
1183                    .send_to(&packet, peer_addr)
1184                    .await
1185                    .map_err(|e| AdapterError::Connection(format!("send failed: {}", e)))?;
1186
1187                // Track for reliability with PRE-encryption inputs.
1188                // Stashing the encrypted bytes was unsound: the
1189                // receiver's replay window rejects retransmits that
1190                // carry a stale wire counter. The descriptor lets
1191                // the retransmit driver call `builder.build` again
1192                // with a fresh counter.
1193                if reliable {
1194                    // Per perf #133 — Arc-wrap the descriptor before
1195                    // handing it to the reliability mode. The
1196                    // retransmit window then shares the inner
1197                    // `Vec<Bytes>` rather than holding an owned copy.
1198                    let descriptor = std::sync::Arc::new(reliability::RetransmitDescriptor {
1199                        seq,
1200                        stream_id,
1201                        events: current_batch.clone(),
1202                        flags,
1203                    });
1204                    let stream = session.get_or_create_stream(stream_id);
1205                    stream.with_reliability(|r| r.on_send(descriptor));
1206                }
1207
1208                current_batch.clear();
1209                current_size = 0;
1210            }
1211
1212            current_batch.push(event_bytes);
1213            current_size += frame_size;
1214        }
1215
1216        // Send remaining events
1217        if !current_batch.is_empty() {
1218            let seq;
1219            {
1220                let stream = session.get_or_create_stream(stream_id);
1221                seq = stream.next_tx_seq();
1222            }
1223
1224            let flags = if reliable {
1225                PacketFlags::RELIABLE
1226            } else {
1227                PacketFlags::NONE
1228            };
1229
1230            let packet = builder.build(stream_id, seq, &current_batch, flags);
1231
1232            socket
1233                .send_to(&packet, peer_addr)
1234                .await
1235                .map_err(|e| AdapterError::Connection(format!("send failed: {}", e)))?;
1236
1237            if reliable {
1238                // Per perf #133 — see the matching call site above.
1239                let descriptor = std::sync::Arc::new(reliability::RetransmitDescriptor {
1240                    seq,
1241                    stream_id,
1242                    events: current_batch.clone(),
1243                    flags,
1244                });
1245                let stream = session.get_or_create_stream(stream_id);
1246                stream.with_reliability(|r| r.on_send(descriptor));
1247            }
1248        }
1249
1250        session.touch();
1251
1252        Ok(())
1253    }
1254
1255    async fn poll_shard(
1256        &self,
1257        shard_id: u16,
1258        from_id: Option<&str>,
1259        limit: usize,
1260    ) -> Result<ShardPollResult, AdapterError> {
1261        let mut events = Vec::with_capacity(limit);
1262
1263        if let Some(queue) = self.inbound.get(&shard_id) {
1264            while events.len() < limit {
1265                if let Some(event) = queue.pop() {
1266                    if from_id.is_none() || event_id_gt(&event.id, from_id.unwrap_or("")) {
1267                        events.push(event);
1268                    }
1269                    // Events at or before the cursor have already been
1270                    // consumed — drop them instead of requeuing. Requeuing
1271                    // caused unbounded memory growth because these events
1272                    // can never pass an advancing cursor.
1273                } else {
1274                    break;
1275                }
1276            }
1277        }
1278
1279        let has_more = self
1280            .inbound
1281            .get(&shard_id)
1282            .map(|q| !q.is_empty())
1283            .unwrap_or(false);
1284        let next_id = events.last().map(|e| e.id.clone());
1285
1286        Ok(ShardPollResult {
1287            events,
1288            next_id,
1289            has_more,
1290        })
1291    }
1292
1293    async fn flush(&self) -> Result<(), AdapterError> {
1294        // For reliable streams, wait for all pending ACKs
1295        // Currently a no-op since we're fire-and-forget by default
1296        Ok(())
1297    }
1298
1299    async fn shutdown(&self) -> Result<(), AdapterError> {
1300        self.shutdown.store(true, Ordering::Release);
1301
1302        // Wake all tasks blocked on I/O so they can observe the shutdown flag.
1303        // notify_waiters wakes all current waiters (receiver + heartbeat).
1304        self.shutdown_notify.notify_waiters();
1305
1306        // Clear session
1307        self.session_manager.clear_session();
1308
1309        // Wait for tasks to complete
1310        let mut tasks = self.tasks.lock().await;
1311        for task in tasks.drain(..) {
1312            let _ = task.await;
1313        }
1314
1315        self.initialized.store(false, Ordering::Release);
1316
1317        tracing::info!("Net adapter shutdown complete");
1318
1319        Ok(())
1320    }
1321
1322    fn name(&self) -> &'static str {
1323        "net"
1324    }
1325
1326    async fn is_healthy(&self) -> bool {
1327        self.initialized.load(Ordering::Acquire) && self.session_manager.check_session()
1328    }
1329}
1330
1331impl std::fmt::Debug for NetAdapter {
1332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1333        f.debug_struct("NetAdapter")
1334            .field("config", &self.config)
1335            .field("initialized", &self.initialized.load(Ordering::Relaxed))
1336            .finish()
1337    }
1338}
1339
1340/// Compare two event IDs numerically.
1341///
1342/// IDs are formatted as `"seq:idx"`. Lexicographic comparison is wrong for
1343/// numeric values (e.g. `"9:0" > "10:0"` lexicographically). This function
1344/// parses the components and compares numerically, falling back to string
1345/// comparison only if parsing fails.
1346fn event_id_gt(a: &str, b: &str) -> bool {
1347    fn parse_id(id: &str) -> Option<(u64, u64)> {
1348        let (seq, idx) = id.split_once(':')?;
1349        Some((seq.parse().ok()?, idx.parse().ok()?))
1350    }
1351
1352    match (parse_id(a), parse_id(b)) {
1353        (Some(a), Some(b)) => a > b,
1354        _ => a > b, // fallback to lexicographic
1355    }
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360    use super::*;
1361
1362    #[test]
1363    fn test_adapter_creation() {
1364        let psk = [0x42u8; 32];
1365        let peer_pubkey = [0x24u8; 32];
1366
1367        let config = NetAdapterConfig::initiator(
1368            "127.0.0.1:0".parse().unwrap(),
1369            "127.0.0.1:9999".parse().unwrap(),
1370            psk,
1371            peer_pubkey,
1372        );
1373
1374        let adapter = NetAdapter::new(config).unwrap();
1375        assert_eq!(adapter.name(), "net");
1376    }
1377
1378    /// PERF_AUDIT §2.7 — cache hit: a read younger than the
1379    /// refresh window must return the cached value WITHOUT
1380    /// touching the wall clock and without rewriting the cache.
1381    /// Deterministic — drives `coarse_clock_advance` with
1382    /// synthetic instants instead of racing a real 1 ms window
1383    /// against OS scheduling.
1384    #[test]
1385    fn coarse_clock_reuses_cache_within_refresh_window() {
1386        let t0 = std::time::Instant::now();
1387        let within = t0 + std::time::Duration::from_nanos(COARSE_CLOCK_REFRESH_NS - 1);
1388        let (store, ns) = coarse_clock_advance(Some((t0, 42)), within, || {
1389            panic!("cache hit must not read the wall clock")
1390        });
1391        assert_eq!(ns, 42, "hit must return the cached reading");
1392        assert!(store.is_none(), "hit must keep the hit path store-free");
1393    }
1394
1395    /// PERF_AUDIT §2.7 — refresh: a read at (boundary is
1396    /// exclusive) or past the window must take a fresh wall-clock
1397    /// reading and rebase the window on the read instant.
1398    #[test]
1399    fn coarse_clock_refreshes_at_and_past_the_window() {
1400        let t0 = std::time::Instant::now();
1401        let at_boundary = t0 + std::time::Duration::from_nanos(COARSE_CLOCK_REFRESH_NS);
1402        let (store, ns) = coarse_clock_advance(Some((t0, 42)), at_boundary, || 100);
1403        assert_eq!(ns, 100, "boundary read must refresh");
1404        assert_eq!(
1405            store,
1406            Some((at_boundary, 100)),
1407            "refresh must rebase the window on the read instant"
1408        );
1409    }
1410
1411    /// PERF_AUDIT §2.7 — cold start: no cached pair → wall-clock
1412    /// read, cached against the read instant.
1413    #[test]
1414    fn coarse_clock_cold_start_reads_wall_clock() {
1415        let t0 = std::time::Instant::now();
1416        let (store, ns) = coarse_clock_advance(None, t0, || 7);
1417        assert_eq!(ns, 7);
1418        assert_eq!(store, Some((t0, 7)));
1419    }
1420
1421    #[test]
1422    fn current_timestamp_advances_after_refresh_interval() {
1423        // After sleeping past the refresh interval, the next read
1424        // must return a larger value — pins that the cache
1425        // actually refreshes rather than getting stuck on the
1426        // initial reading.
1427        let first = current_timestamp();
1428        std::thread::sleep(std::time::Duration::from_millis(5));
1429        let later = current_timestamp();
1430        assert!(
1431            later > first,
1432            "post-refresh reading must advance: first={}, later={}",
1433            first,
1434            later
1435        );
1436    }
1437
1438    #[test]
1439    fn test_shard_id_from_stream_id_uses_modulo() {
1440        // Regression: shard_id was computed as `stream_id as u16` (truncation),
1441        // which collides for stream IDs that differ only in upper bits.
1442        // The fix uses `stream_id % num_shards`.
1443        let num_shards: u16 = 8;
1444
1445        // Two stream IDs that are identical in their low 16 bits
1446        // but different overall must map to the same shard via modulo,
1447        // while truncation would also give the same result here.
1448        // More importantly, a large stream_id must stay within [0, num_shards).
1449        let stream_a: u64 = 0xDEAD_BEEF_0000_0003;
1450        let stream_b: u64 = 0xCAFE_BABE_0000_0003;
1451
1452        let shard_a = (stream_a % num_shards as u64) as u16;
1453        let shard_b = (stream_b % num_shards as u64) as u16;
1454
1455        assert!(
1456            shard_a < num_shards,
1457            "shard must be in range [0, num_shards)"
1458        );
1459        assert!(
1460            shard_b < num_shards,
1461            "shard must be in range [0, num_shards)"
1462        );
1463
1464        // Large stream IDs that would overflow u16 must still be valid shard IDs
1465        let big_stream: u64 = 0xFFFF_FFFF_FFFF_FFFF;
1466        let shard_big = (big_stream % num_shards as u64) as u16;
1467        assert!(shard_big < num_shards);
1468
1469        // Truncation would give 0xFFFF = 65535, which is >= num_shards.
1470        // Modulo gives a valid shard.
1471        assert_ne!(
1472            big_stream as u16, shard_big,
1473            "modulo must differ from truncation for large stream IDs"
1474        );
1475    }
1476
1477    #[test]
1478    fn test_invalid_config() {
1479        let psk = [0x42u8; 32];
1480        let peer_pubkey = [0x24u8; 32];
1481
1482        let mut config = NetAdapterConfig::initiator(
1483            "127.0.0.1:0".parse().unwrap(),
1484            "127.0.0.1:9999".parse().unwrap(),
1485            psk,
1486            peer_pubkey,
1487        );
1488        config.peer_static_pubkey = None;
1489
1490        let result = NetAdapter::new(config);
1491        assert!(result.is_err());
1492    }
1493
1494    // Regression: event_id_gt used lexicographic comparison, so "9:0" > "10:0"
1495    // was true (wrong). Now uses numeric comparison (BUGS_4 #2).
1496    #[test]
1497    fn test_event_id_gt_numeric_ordering() {
1498        // Basic ordering
1499        assert!(event_id_gt("2:0", "1:0"));
1500        assert!(!event_id_gt("1:0", "2:0"));
1501        assert!(!event_id_gt("1:0", "1:0"));
1502
1503        // The critical case: double-digit seq must compare correctly
1504        assert!(event_id_gt("10:0", "9:0"));
1505        assert!(event_id_gt("100:0", "99:0"));
1506        assert!(!event_id_gt("9:0", "10:0"));
1507
1508        // Index comparison within same sequence
1509        assert!(event_id_gt("5:2", "5:1"));
1510        assert!(!event_id_gt("5:1", "5:2"));
1511
1512        // Large sequences
1513        assert!(event_id_gt("1000000:0", "999999:0"));
1514    }
1515
1516    // Regression: poll_shard used to destructively pop events that didn't
1517    // pass the cursor filter, causing permanent data loss (BUGS_4 #1).
1518    // This is tested indirectly via event_id_gt since poll_shard requires
1519    // a full adapter setup, but the non-destructive requeue logic is
1520    // verified by the SegQueue re-push in the implementation.
1521    #[test]
1522    fn test_event_id_gt_edge_cases() {
1523        // Empty strings
1524        assert!(event_id_gt("1:0", ""));
1525        // Malformed IDs fall back to string comparison
1526        assert!(event_id_gt("b", "a"));
1527        assert!(!event_id_gt("a", "b"));
1528    }
1529
1530    /// Regression: packets built by PacketBuilder must survive process_packet.
1531    /// This test bypasses the network and directly verifies the encrypt→decrypt
1532    /// data path, catching AAD mismatches, nonce construction bugs, and key
1533    /// derivation errors.
1534    #[test]
1535    fn test_build_then_process_packet_roundtrip() {
1536        use crate::adapter::net::crypto::{NoiseHandshake, StaticKeypair};
1537        use dashmap::DashMap;
1538        use std::sync::Arc;
1539
1540        // Perform a real handshake to get matching keys
1541        let psk = [0x42u8; 32];
1542        let responder_kp = StaticKeypair::generate();
1543
1544        let mut initiator = NoiseHandshake::initiator(&psk, &responder_kp.public).unwrap();
1545        let mut responder = NoiseHandshake::responder(&psk, &responder_kp).unwrap();
1546
1547        let msg1 = initiator.write_message(&[]).unwrap();
1548        responder.read_message(&msg1).unwrap();
1549        let msg2 = responder.write_message(&[]).unwrap();
1550        initiator.read_message(&msg2).unwrap();
1551
1552        let init_keys = initiator.into_session_keys().unwrap();
1553        let resp_keys = responder.into_session_keys().unwrap();
1554
1555        // Initiator builds a packet
1556        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1557        let events = vec![
1558            Bytes::from(r#"{"token":"hello"}"#),
1559            Bytes::from(r#"{"token":"world"}"#),
1560        ];
1561        let packet = builder.build(0, 0, &events, PacketFlags::NONE);
1562
1563        // Responder processes the packet
1564        let resp_session = Arc::new(NetSession::new(
1565            resp_keys,
1566            "127.0.0.1:5000".parse().unwrap(),
1567            4,
1568            false,
1569        ));
1570        let inbound: InboundQueues = Arc::new(DashMap::new());
1571        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1572
1573        NetAdapter::process_packet(packet, source, &resp_session, &inbound, 1);
1574
1575        // Events should be queued in shard 0
1576        let queue = inbound.get(&0).expect("shard 0 should have events");
1577        assert_eq!(queue.len(), 2, "expected 2 events, got {}", queue.len());
1578
1579        let e1 = queue.pop().unwrap();
1580        assert_eq!(&e1.raw[..], br#"{"token":"hello"}"#);
1581
1582        let e2 = queue.pop().unwrap();
1583        assert_eq!(&e2.raw[..], br#"{"token":"world"}"#);
1584    }
1585
1586    /// Helper: perform a Noise handshake and return matched key pairs.
1587    fn make_session_keys() -> (SessionKeys, SessionKeys) {
1588        use crate::adapter::net::crypto::{NoiseHandshake, StaticKeypair};
1589
1590        let psk = [0x42u8; 32];
1591        let responder_kp = StaticKeypair::generate();
1592
1593        let mut initiator = NoiseHandshake::initiator(&psk, &responder_kp.public).unwrap();
1594        let mut responder = NoiseHandshake::responder(&psk, &responder_kp).unwrap();
1595
1596        let msg1 = initiator.write_message(&[]).unwrap();
1597        responder.read_message(&msg1).unwrap();
1598        let msg2 = responder.write_message(&[]).unwrap();
1599        initiator.read_message(&msg2).unwrap();
1600
1601        (
1602            initiator.into_session_keys().unwrap(),
1603            responder.into_session_keys().unwrap(),
1604        )
1605    }
1606
1607    #[test]
1608    fn test_process_packet_rejects_truncated_packet() {
1609        use dashmap::DashMap;
1610        use std::sync::Arc;
1611
1612        let (init_keys, resp_keys) = make_session_keys();
1613
1614        // Build a valid packet, then truncate it
1615        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1616        let packet = builder.build(0, 0, &[Bytes::from_static(b"hello")], PacketFlags::NONE);
1617
1618        let resp_session = Arc::new(NetSession::new(
1619            resp_keys,
1620            "127.0.0.1:5000".parse().unwrap(),
1621            4,
1622            false,
1623        ));
1624        let inbound: InboundQueues = Arc::new(DashMap::new());
1625        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1626
1627        // Truncate: remove last 10 bytes (partial auth tag)
1628        let truncated = packet.slice(..packet.len() - 10);
1629        NetAdapter::process_packet(truncated, source, &resp_session, &inbound, 1);
1630        assert!(
1631            inbound.get(&0).is_none() || inbound.get(&0).unwrap().is_empty(),
1632            "truncated packet must be silently dropped"
1633        );
1634    }
1635
1636    #[test]
1637    fn test_process_packet_rejects_tampered_payload() {
1638        use dashmap::DashMap;
1639        use std::sync::Arc;
1640
1641        let (init_keys, resp_keys) = make_session_keys();
1642
1643        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1644        let packet = builder.build(0, 0, &[Bytes::from_static(b"hello")], PacketFlags::NONE);
1645
1646        let resp_session = Arc::new(NetSession::new(
1647            resp_keys,
1648            "127.0.0.1:5000".parse().unwrap(),
1649            4,
1650            false,
1651        ));
1652        let inbound: InboundQueues = Arc::new(DashMap::new());
1653        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1654
1655        // Tamper: flip a byte in the encrypted payload
1656        let mut tampered = bytes::BytesMut::from(&packet[..]);
1657        tampered[super::protocol::HEADER_SIZE + 2] ^= 0xFF;
1658        NetAdapter::process_packet(tampered.freeze(), source, &resp_session, &inbound, 1);
1659
1660        assert!(
1661            inbound.get(&0).is_none() || inbound.get(&0).unwrap().is_empty(),
1662            "tampered packet must be rejected by AEAD"
1663        );
1664    }
1665
1666    #[test]
1667    fn test_process_packet_rejects_wrong_session_id() {
1668        use dashmap::DashMap;
1669        use std::sync::Arc;
1670
1671        let (init_keys, resp_keys) = make_session_keys();
1672
1673        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1674        let packet = builder.build(0, 0, &[Bytes::from_static(b"hello")], PacketFlags::NONE);
1675
1676        // Create session with a DIFFERENT session_id
1677        let mut wrong_keys = resp_keys;
1678        wrong_keys.session_id = 0xDEAD;
1679        let resp_session = Arc::new(NetSession::new(
1680            wrong_keys,
1681            "127.0.0.1:5000".parse().unwrap(),
1682            4,
1683            false,
1684        ));
1685        let inbound: InboundQueues = Arc::new(DashMap::new());
1686        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1687
1688        NetAdapter::process_packet(packet, source, &resp_session, &inbound, 1);
1689
1690        assert!(
1691            inbound.get(&0).is_none() || inbound.get(&0).unwrap().is_empty(),
1692            "packet with wrong session_id must be dropped"
1693        );
1694    }
1695
1696    #[test]
1697    fn test_process_packet_multi_packet_batch_all_events_arrive() {
1698        use dashmap::DashMap;
1699        use std::sync::Arc;
1700
1701        let (init_keys, resp_keys) = make_session_keys();
1702
1703        let resp_session = Arc::new(NetSession::new(
1704            resp_keys,
1705            "127.0.0.1:5000".parse().unwrap(),
1706            4,
1707            false,
1708        ));
1709        let inbound: InboundQueues = Arc::new(DashMap::new());
1710        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1711
1712        // Build events large enough to span multiple packets.
1713        // Each event is ~200 bytes, MAX_PAYLOAD_SIZE is ~8112, so ~40 per packet.
1714        // 200 events → ~5 packets.
1715        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1716        let total_events = 200;
1717        let mut seq = 0u64;
1718
1719        // Simulate on_batch splitting into multiple packets
1720        let mut current_batch: Vec<Bytes> = Vec::new();
1721        let mut current_size = 0;
1722
1723        for i in 0..total_events {
1724            let data = format!("{{\"i\":{},\"pad\":\"{}\"}}", i, "x".repeat(150));
1725            let event_bytes = Bytes::from(data);
1726            let frame_size = EventFrame::LEN_SIZE + event_bytes.len();
1727
1728            if current_size + frame_size > protocol::MAX_PAYLOAD_SIZE && !current_batch.is_empty() {
1729                let packet = builder.build(0, seq, &current_batch, PacketFlags::NONE);
1730                NetAdapter::process_packet(packet, source, &resp_session, &inbound, 1);
1731                seq += 1;
1732                current_batch.clear();
1733                current_size = 0;
1734            }
1735
1736            current_batch.push(event_bytes);
1737            current_size += frame_size;
1738        }
1739
1740        if !current_batch.is_empty() {
1741            let packet = builder.build(0, seq, &current_batch, PacketFlags::NONE);
1742            NetAdapter::process_packet(packet, source, &resp_session, &inbound, 1);
1743        }
1744
1745        // All events must arrive
1746        let queue = inbound.get(&0).expect("shard 0 should have events");
1747        assert_eq!(
1748            queue.len(),
1749            total_events,
1750            "all {} events must arrive across multiple packets",
1751            total_events
1752        );
1753    }
1754
1755    #[test]
1756    fn test_build_then_process_packet_both_directions() {
1757        use dashmap::DashMap;
1758        use std::sync::Arc;
1759
1760        let (init_keys, resp_keys) = make_session_keys();
1761        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1762
1763        // Direction 1: initiator → responder
1764        {
1765            let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1766            let packet = builder.build(0, 0, &[Bytes::from_static(b"i2r")], PacketFlags::NONE);
1767
1768            let session = Arc::new(NetSession::new(resp_keys.clone(), source, 4, false));
1769            let inbound: InboundQueues = Arc::new(DashMap::new());
1770            NetAdapter::process_packet(packet, source, &session, &inbound, 1);
1771
1772            let queue = inbound.get(&0).expect("i2r: shard 0 should have events");
1773            assert_eq!(queue.len(), 1, "i2r: expected 1 event");
1774            assert_eq!(&queue.pop().unwrap().raw[..], b"i2r");
1775        }
1776
1777        // Direction 2: responder → initiator
1778        {
1779            let mut builder = PacketBuilder::new(&resp_keys.tx_key, resp_keys.session_id);
1780            let packet = builder.build(0, 0, &[Bytes::from_static(b"r2i")], PacketFlags::NONE);
1781
1782            let session = Arc::new(NetSession::new(init_keys.clone(), source, 4, false));
1783            let inbound: InboundQueues = Arc::new(DashMap::new());
1784            NetAdapter::process_packet(packet, source, &session, &inbound, 1);
1785
1786            let queue = inbound.get(&0).expect("r2i: shard 0 should have events");
1787            assert_eq!(queue.len(), 1, "r2i: expected 1 event");
1788            assert_eq!(&queue.pop().unwrap().raw[..], b"r2i");
1789        }
1790    }
1791
1792    #[test]
1793    fn test_poll_shard_cursor_drops_consumed_events() {
1794        // Verify that poll_shard with a cursor drops events at or before
1795        // the cursor (they've already been consumed) and returns only
1796        // events after the cursor. The queue should be empty afterward —
1797        // no unbounded requeue growth.
1798        use std::sync::Arc;
1799
1800        let (init_keys, resp_keys) = make_session_keys();
1801
1802        let resp_session = Arc::new(NetSession::new(
1803            resp_keys,
1804            "127.0.0.1:5000".parse().unwrap(),
1805            4,
1806            false,
1807        ));
1808        let inbound: InboundQueues = Arc::new(DashMap::new());
1809        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1810
1811        // Send 3 packets (sequences 0, 1, 2), each with 1 event
1812        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1813        for seq in 0..3u64 {
1814            let events = vec![Bytes::from(format!("event-{}", seq))];
1815            let packet = builder.build(0, seq, &events, PacketFlags::NONE);
1816            NetAdapter::process_packet(packet, source, &resp_session, &inbound, 1);
1817        }
1818
1819        let queue = inbound.get(&0u16).unwrap();
1820        assert_eq!(queue.len(), 3);
1821
1822        // Simulate poll_shard with cursor "0:0" — drops event 0:0,
1823        // returns events 1:0 and 2:0
1824        let from_id = "0:0";
1825        let mut events = Vec::new();
1826        while events.len() < 10 {
1827            if let Some(event) = queue.pop() {
1828                if event_id_gt(&event.id, from_id) {
1829                    events.push(event);
1830                }
1831                // Events at/before cursor are dropped (not requeued)
1832            } else {
1833                break;
1834            }
1835        }
1836
1837        assert_eq!(events.len(), 2, "should get 2 events after cursor 0:0");
1838        assert_eq!(events[0].id, "1:0");
1839        assert_eq!(events[1].id, "2:0");
1840
1841        // Queue should be empty — consumed events are dropped, not requeued
1842        assert_eq!(queue.len(), 0, "queue should be empty after poll drains it");
1843    }
1844
1845    #[test]
1846    fn test_process_packet_old_counter_rejected() {
1847        // Verify that a packet with a counter below the replay window
1848        // is rejected after the window has advanced.
1849        use std::sync::Arc;
1850
1851        let (init_keys, resp_keys) = make_session_keys();
1852        let resp_session = Arc::new(NetSession::new(
1853            resp_keys,
1854            "127.0.0.1:5000".parse().unwrap(),
1855            4,
1856            false,
1857        ));
1858        let inbound: InboundQueues = Arc::new(DashMap::new());
1859        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1860
1861        // Send 1100 packets to advance the rx_counter past the replay window (1024)
1862        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1863        for seq in 0..1100u64 {
1864            let packet = builder.build(0, seq, &[Bytes::from_static(b"x")], PacketFlags::NONE);
1865            NetAdapter::process_packet(packet, source, &resp_session, &inbound, 1);
1866        }
1867        assert_eq!(inbound.get(&0).unwrap().len(), 1100);
1868
1869        // Build a packet with a fresh builder whose counter starts at 0.
1870        // The rx_counter is now at ~1100, so counter 0 is outside the
1871        // 1024-wide replay window and must be rejected.
1872        let mut stale_builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1873        let stale_packet =
1874            stale_builder.build(0, 9999, &[Bytes::from_static(b"stale")], PacketFlags::NONE);
1875        NetAdapter::process_packet(stale_packet, source, &resp_session, &inbound, 1);
1876
1877        // Should still be 1100 — stale packet rejected
1878        assert_eq!(
1879            inbound.get(&0).unwrap().len(),
1880            1100,
1881            "packet with stale counter must be rejected"
1882        );
1883    }
1884
1885    #[test]
1886    fn test_process_packet_far_future_counter_rejected() {
1887        // Verify that a packet with a counter far beyond MAX_FORWARD is
1888        // rejected, preventing an attacker from advancing the rx_counter
1889        // and denying subsequent legitimate packets.
1890        use std::sync::Arc;
1891
1892        let (_init_keys, resp_keys) = make_session_keys();
1893
1894        // Build a valid packet, then manually tamper the nonce counter
1895        // to a huge value. The AEAD will fail because the nonce doesn't
1896        // match, but we're testing that is_valid_rx_counter rejects it
1897        // before even attempting decryption.
1898        let resp_session = Arc::new(NetSession::new(
1899            resp_keys,
1900            "127.0.0.1:5000".parse().unwrap(),
1901            4,
1902            false,
1903        ));
1904
1905        // Directly test the cipher's counter validation
1906        let rx_cipher = resp_session.rx_cipher();
1907        assert!(
1908            !rx_cipher.is_valid_rx_counter(u64::MAX),
1909            "counter at u64::MAX must be rejected (far beyond MAX_FORWARD)"
1910        );
1911        assert!(
1912            rx_cipher.is_valid_rx_counter(0),
1913            "counter 0 should be valid initially"
1914        );
1915    }
1916
1917    /// Regression: BUG_REPORT.md #5 — `process_packet` previously
1918    /// discarded the bool returned by `r.on_receive(seq)` on the
1919    /// reliability layer, queueing events even for duplicates.
1920    /// Each retransmit re-encrypts with a fresh outer counter, so
1921    /// the cipher's replay window does not catch this; without
1922    /// honoring `on_receive`, the inbound queue accumulates
1923    /// duplicates and breaks exactly-once delivery on reliable
1924    /// streams.
1925    ///
1926    /// We construct the duplicate-detection scenario by building
1927    /// two distinct packets that share the same stream sequence.
1928    /// On a reliable session the second one's `on_receive` returns
1929    /// `false`, so `process_packet` must not enqueue its events.
1930    /// (The cipher's outer counter is fresh on both packets, so
1931    /// the replay window can't filter them — only the reliability
1932    /// layer's check stops the duplicate.)
1933    #[test]
1934    fn process_packet_drops_duplicates_per_reliability_decision() {
1935        use dashmap::DashMap;
1936        use std::sync::Arc;
1937
1938        let (init_keys, resp_keys) = make_session_keys();
1939
1940        // Reliable session — its streams use `ReliableStream`,
1941        // whose `on_receive` returns `false` for `seq <
1942        // next_expected` (duplicates).
1943        let resp_session = Arc::new(NetSession::new(
1944            resp_keys,
1945            "127.0.0.1:5000".parse().unwrap(),
1946            4,
1947            true, // default_reliable
1948        ));
1949        let inbound: InboundQueues = Arc::new(DashMap::new());
1950        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
1951
1952        // Two packets on stream 7. First carries sequences 0..1,
1953        // second is a duplicate (same seq=0) that should be
1954        // filtered. We deliver seq=0 then seq=1 first to advance
1955        // `next_expected` past 0, then a packet with seq=0 — that
1956        // last one is the duplicate.
1957        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
1958        let packet0 = builder.build(7, 0, &[Bytes::from(r#"{"first":0}"#)], PacketFlags::NONE);
1959        let packet1 = builder.build(7, 1, &[Bytes::from(r#"{"first":1}"#)], PacketFlags::NONE);
1960        // Re-encrypted retransmit of seq=0 — same stream, same seq,
1961        // different payload. This is the scenario the bug allowed
1962        // through.
1963        let packet0_dup = builder.build(
1964            7,
1965            0,
1966            &[Bytes::from(r#"{"dup":"should_not_appear"}"#)],
1967            PacketFlags::NONE,
1968        );
1969
1970        NetAdapter::process_packet(packet0, source, &resp_session, &inbound, 1);
1971        NetAdapter::process_packet(packet1, source, &resp_session, &inbound, 1);
1972        NetAdapter::process_packet(packet0_dup, source, &resp_session, &inbound, 1);
1973
1974        let queue = inbound.get(&0).expect("shard 0 should exist");
1975        assert_eq!(
1976            queue.len(),
1977            2,
1978            "duplicate packet must NOT enqueue (BUG_REPORT.md #5); \
1979             got {} events, expected exactly 2 (seq=0 and seq=1, no dup)",
1980            queue.len()
1981        );
1982
1983        // Drain in FIFO order and assert no `should_not_appear`
1984        // event sneaked through.
1985        let e0 = queue.pop().unwrap();
1986        assert_eq!(&e0.raw[..], br#"{"first":0}"#);
1987        let e1 = queue.pop().unwrap();
1988        assert_eq!(&e1.raw[..], br#"{"first":1}"#);
1989        assert!(queue.is_empty());
1990    }
1991
1992    /// Regression: heartbeats must be AEAD-authenticated so an
1993    /// off-path attacker who knows or observes the session_id
1994    /// cannot spoof them. Pre-fix the receiver only checked
1995    /// `source == peer_addr` (UDP source — spoofable) and
1996    /// `session_id` match (in cleartext on every packet); now the
1997    /// 16-byte Poly1305 tag binds the heartbeat to the session
1998    /// key.
1999    #[test]
2000    fn heartbeat_is_aead_authenticated() {
2001        use crate::adapter::net::pool::PacketBuilder;
2002        use dashmap::DashMap;
2003        use std::sync::Arc;
2004
2005        let (init_keys, resp_keys) = make_session_keys();
2006
2007        let resp_session = Arc::new(NetSession::new(
2008            resp_keys,
2009            "127.0.0.1:5000".parse().unwrap(),
2010            4,
2011            false,
2012        ));
2013        let inbound: InboundQueues = Arc::new(DashMap::new());
2014        let source: std::net::SocketAddr = "127.0.0.1:5000".parse().unwrap();
2015
2016        // Build a legitimate heartbeat with the initiator's
2017        // session key and tag it.
2018        let mut builder = PacketBuilder::new(&init_keys.tx_key, init_keys.session_id);
2019        let heartbeat = builder.build_heartbeat();
2020        let last_activity_before = resp_session.last_activity_ns();
2021        std::thread::sleep(std::time::Duration::from_millis(2));
2022
2023        // Process: this must succeed and call session.touch().
2024        NetAdapter::process_packet(heartbeat, source, &resp_session, &inbound, 1);
2025        let last_activity_after = resp_session.last_activity_ns();
2026        assert!(
2027            last_activity_after > last_activity_before,
2028            "legitimate AEAD-tagged heartbeat must call session.touch()"
2029        );
2030
2031        // Forge an unauthenticated heartbeat: header-only, no tag.
2032        // Pre-fix this would have passed; post-fix it must be
2033        // rejected.
2034        let mut forged = bytes::BytesMut::new();
2035        let header = NetHeader::heartbeat(resp_session.session_id());
2036        forged.extend_from_slice(&header.to_bytes());
2037        let forged = forged.freeze();
2038        let last_activity_before = resp_session.last_activity_ns();
2039        std::thread::sleep(std::time::Duration::from_millis(2));
2040        NetAdapter::process_packet(forged, source, &resp_session, &inbound, 1);
2041        let last_activity_after = resp_session.last_activity_ns();
2042        assert_eq!(
2043            last_activity_before, last_activity_after,
2044            "unauthenticated heartbeat (no AEAD tag) must NOT touch the session"
2045        );
2046
2047        // Forge a heartbeat with the right session_id but a
2048        // garbage 16-byte "tag". Tag verification fails.
2049        let mut forged_tag = bytes::BytesMut::new();
2050        let mut header_bytes = NetHeader::heartbeat(resp_session.session_id()).to_bytes();
2051        // Stamp a plausible nonce so the receiver gets to the
2052        // decrypt step (otherwise it bails earlier on counter).
2053        header_bytes[12..16].copy_from_slice(&[0u8; 4]);
2054        header_bytes[16..24].copy_from_slice(&1u64.to_le_bytes());
2055        forged_tag.extend_from_slice(&header_bytes);
2056        forged_tag.extend_from_slice(&[0xAAu8; 16]); // garbage tag
2057        let forged_tag = forged_tag.freeze();
2058        let last_activity_before = resp_session.last_activity_ns();
2059        std::thread::sleep(std::time::Duration::from_millis(2));
2060        NetAdapter::process_packet(forged_tag, source, &resp_session, &inbound, 1);
2061        let last_activity_after = resp_session.last_activity_ns();
2062        assert_eq!(
2063            last_activity_before, last_activity_after,
2064            "heartbeat with garbage AEAD tag must NOT touch the session"
2065        );
2066    }
2067
2068    /// Regression: the handshake responder must rate-limit per
2069    /// source so a flooder can't monopolize the recv loop.
2070    /// `HandshakePacer` is the building block: it tracks
2071    /// `(count, window_start)` per source and rejects after
2072    /// `max_per_window` attempts within `window`.
2073    #[test]
2074    fn handshake_pacer_rejects_floods_per_source() {
2075        use std::time::Duration;
2076        let mut pacer = HandshakePacer::new(3, Duration::from_millis(50));
2077
2078        let attacker: std::net::SocketAddr = "10.0.0.1:9000".parse().unwrap();
2079        let legit: std::net::SocketAddr = "10.0.0.2:9000".parse().unwrap();
2080
2081        // Attacker fires 3 attempts — all allowed (within budget).
2082        for _ in 0..3 {
2083            assert!(pacer.check_and_record(attacker));
2084        }
2085        // Fourth and beyond — rejected.
2086        for _ in 0..10 {
2087            assert!(
2088                !pacer.check_and_record(attacker),
2089                "attacker exceeding budget must be dropped"
2090            );
2091        }
2092
2093        // The legitimate initiator (different source) is unaffected
2094        // by the attacker's burst — the budget is per-source.
2095        assert!(
2096            pacer.check_and_record(legit),
2097            "legitimate source must still get through despite attacker flood"
2098        );
2099
2100        // After the window expires the attacker's budget refills.
2101        std::thread::sleep(Duration::from_millis(55));
2102        assert!(
2103            pacer.check_and_record(attacker),
2104            "attacker budget must refill after window"
2105        );
2106    }
2107}