Skip to main content

rns_net/common/
link_manager.rs

1//! Link manager: wires rns-core LinkEngine + Channel + Resource into the driver.
2//!
3//! Manages multiple concurrent links, link destination registration,
4//! request/response handling, resource transfers, and full lifecycle
5//! (handshake → active → teardown).
6//!
7//! Python reference: Link.py, RequestReceipt.py, Resource.py
8
9use std::collections::HashMap;
10use std::fs::File;
11use std::io::{Read, Write};
12use std::path::PathBuf;
13
14use super::compressor::Bzip2Compressor;
15use rns_core::channel::{Channel, Sequence};
16use rns_core::constants;
17use rns_core::link::types::{LinkId, LinkState, TeardownReason};
18use rns_core::link::{LinkAction, LinkEngine, LinkMode};
19use rns_core::packet::{PacketFlags, RawPacket};
20use rns_core::resource::{ResourceAction, ResourceReceiver, ResourceSender};
21use rns_crypto::ed25519::Ed25519PrivateKey;
22use rns_crypto::{OsRng, Rng};
23
24use super::time;
25
26mod channel_handling;
27mod receive_worker;
28mod request_handling;
29mod resource_handling;
30mod state;
31
32use crate::resource::{
33    ReceivedResourceFile, ResourceReceiveMode, ResourceTransferError, ResourceTransferId,
34};
35use resource_handling::ResourceSendParams;
36use state::*;
37
38// Bound deferred handshake traffic even when an initiator never sends LRRTT.
39const MAX_PRE_RTT_PACKETS: usize = 8;
40const MAX_PRE_RTT_BYTES: usize = 64 * 1024;
41
42/// Resource acceptance strategy.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum ResourceStrategy {
45    /// Reject all incoming resources.
46    #[default]
47    AcceptNone,
48    /// Accept all incoming resources automatically.
49    AcceptAll,
50    /// Query the application callback for each resource.
51    AcceptApp,
52}
53
54struct IncomingSplitTransfer {
55    total_segments: u64,
56    completed_segments: u64,
57    current_segment_index: u64,
58    current_received_parts: usize,
59    current_total_parts: usize,
60    storage: IncomingSplitStorage,
61    metadata: Option<Vec<u8>>,
62    is_request: bool,
63    is_response: bool,
64}
65
66enum IncomingSplitStorage {
67    Memory(Vec<u8>),
68    File {
69        file: File,
70        path: PathBuf,
71        size: u64,
72    },
73}
74
75struct OutgoingSplitTransfer {
76    total_segments: u64,
77    completed_segments: u64,
78    current_segment_index: u64,
79    current_sent_parts: usize,
80    current_total_parts: usize,
81}
82
83struct OutgoingStreamTransfer {
84    transfer_id: ResourceTransferId,
85    reader: Box<dyn Read + Send>,
86    remaining: u64,
87    declared_length: u64,
88    metadata_overhead: u64,
89    auto_compress: bool,
90}
91
92/// Response produced by an application request handler.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum RequestResponse {
95    /// Send the response as the normal request response value.
96    Bytes(Vec<u8>),
97    /// Send the response as a resource response with optional metadata.
98    Resource {
99        data: Vec<u8>,
100        metadata: Option<Vec<u8>>,
101        auto_compress: bool,
102    },
103    /// Send raw file bytes with MessagePack metadata, matching Python's
104    /// `[file_handle, metadata]` response. No response-value envelope is added.
105    File {
106        data: Vec<u8>,
107        metadata: Vec<u8>,
108        auto_compress: bool,
109    },
110}
111
112/// Reason an outbound request failed before receiving an accepted response.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum RequestFailure {
115    /// The peer advertised or sent a response larger than the configured limit.
116    ResponseTooLarge { size: u64, maximum: usize },
117}
118
119impl From<Vec<u8>> for RequestResponse {
120    fn from(data: Vec<u8>) -> Self {
121        RequestResponse::Bytes(data)
122    }
123}
124
125/// Actions produced by LinkManager for the driver to dispatch.
126#[derive(Debug)]
127pub enum LinkManagerAction {
128    /// Send a packet via the transport engine outbound path.
129    SendPacket {
130        raw: Vec<u8>,
131        dest_type: u8,
132        attached_interface: Option<rns_core::transport::types::InterfaceId>,
133    },
134    /// Link established — notify callbacks.
135    LinkEstablished {
136        link_id: LinkId,
137        dest_hash: [u8; 16],
138        hops: u8,
139        rebalanced_at: Option<f64>,
140        rtt: f64,
141        is_initiator: bool,
142    },
143    /// Link closed — notify callbacks.
144    LinkClosed {
145        link_id: LinkId,
146        reason: Option<TeardownReason>,
147    },
148    /// Remote peer identified — notify callbacks.
149    RemoteIdentified {
150        link_id: LinkId,
151        identity_hash: [u8; 16],
152        public_key: [u8; 64],
153    },
154    /// Register a link_id as local destination in transport (for receiving link data).
155    RegisterLinkDest { link_id: LinkId },
156    /// Deregister a link_id from transport local destinations.
157    DeregisterLinkDest { link_id: LinkId },
158    /// A management request that needs to be handled by the driver.
159    /// The driver has access to engine state needed to build the response.
160    ManagementRequest {
161        link_id: LinkId,
162        path_hash: [u8; 16],
163        /// The request data (msgpack-encoded Value from the request array).
164        data: Vec<u8>,
165        /// The request_id (truncated hash of the packed request).
166        request_id: [u8; 16],
167        remote_identity: Option<([u8; 16], [u8; 64])>,
168    },
169    /// Resource data fully received and assembled.
170    ResourceReceived {
171        link_id: LinkId,
172        data: Vec<u8>,
173        metadata: Option<Vec<u8>>,
174    },
175    /// A disk-backed Resource is ready for application ownership.
176    ResourceFileReceived {
177        link_id: LinkId,
178        resource: ReceivedResourceFile,
179    },
180    /// Resource transfer completed (proof validated on sender side).
181    ResourceCompleted { link_id: LinkId },
182    /// Resource transfer failed.
183    ResourceFailed { link_id: LinkId, error: String },
184    /// Resource transfer progress update.
185    ResourceProgress {
186        link_id: LinkId,
187        received: usize,
188        total: usize,
189    },
190    ResourceStreamCompleted {
191        link_id: LinkId,
192        transfer_id: ResourceTransferId,
193    },
194    ResourceStreamFailed {
195        link_id: LinkId,
196        transfer_id: ResourceTransferId,
197        error: ResourceTransferError,
198    },
199    ResourceStreamProgress {
200        link_id: LinkId,
201        transfer_id: ResourceTransferId,
202        transferred: u64,
203        total: u64,
204    },
205    /// Query application whether to accept an incoming resource (for AcceptApp strategy).
206    ResourceAcceptQuery {
207        link_id: LinkId,
208        resource_hash: Vec<u8>,
209        transfer_size: u64,
210        has_metadata: bool,
211    },
212    /// Channel message received on a link.
213    ChannelMessageReceived {
214        link_id: LinkId,
215        msgtype: u16,
216        payload: Vec<u8>,
217    },
218    /// Generic link data received (CONTEXT_NONE).
219    LinkDataReceived {
220        link_id: LinkId,
221        context: u8,
222        data: Vec<u8>,
223    },
224    /// Response received on a link.
225    ResponseReceived {
226        link_id: LinkId,
227        request_id: [u8; 16],
228        data: Vec<u8>,
229        metadata: Option<Vec<u8>>,
230    },
231    /// An outbound request failed.
232    RequestFailed {
233        link_id: LinkId,
234        request_id: [u8; 16],
235        reason: RequestFailure,
236    },
237    /// A link request was received (for hook notification).
238    LinkRequestReceived {
239        link_id: LinkId,
240        receiving_interface: rns_core::transport::types::InterfaceId,
241    },
242    /// A malformed link request was rejected on an interface.
243    ProtocolViolation {
244        receiving_interface: rns_core::transport::types::InterfaceId,
245    },
246}
247
248/// Manages multiple links, link destinations, and request/response.
249pub struct LinkManager {
250    receive_worker: Option<receive_worker::Worker>,
251    receive_worker_wake: Option<crate::event::EventSender>,
252    /// The authoritative O(1) link-id index for pending, active, and closing
253    /// links. Link state lives in each `ManagedLink`, so lifecycle transitions
254    /// cannot desynchronise separate list and lookup-map representations.
255    links: HashMap<LinkId, ManagedLink>,
256    link_destinations: HashMap<[u8; 16], LinkDestination>,
257    request_handlers: Vec<RequestHandlerEntry>,
258    deferred_request_handlers: Vec<DeferredRequestHandlerEntry>,
259    /// Path hashes that should be handled externally (by the driver) rather than
260    /// by registered handler closures. Used for management destinations.
261    management_paths: Vec<[u8; 16]>,
262    link_mtu_discovery: bool,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct LinkRouteHint {
267    pub interface: rns_core::transport::types::InterfaceId,
268    pub transport_id: Option<[u8; 16]>,
269}
270
271impl LinkManager {
272    fn resource_sdu_for_link(link: &ManagedLink) -> usize {
273        // Python parity: Resource.sdu = link.mtu - HEADER_MAXSIZE - IFAC_MIN_SIZE
274        // when MTU signalling is available on the link.
275        let mtu = link.engine.mtu() as usize;
276        let derived = mtu.saturating_sub(constants::HEADER_MAXSIZE + constants::IFAC_MIN_SIZE);
277        if derived > 0 {
278            derived
279        } else {
280            constants::RESOURCE_SDU
281        }
282    }
283
284    fn split_progress_parts(
285        segment_index: u64,
286        total_segments: u64,
287        current_done: usize,
288        current_total: usize,
289        sdu: usize,
290    ) -> (usize, usize) {
291        let max_parts_per_segment = constants::RESOURCE_MAX_EFFICIENT_SIZE.div_ceil(sdu.max(1));
292        let total = (total_segments as usize).saturating_mul(max_parts_per_segment);
293        let completed_segments = segment_index.saturating_sub(1) as usize;
294        let completed = completed_segments.saturating_mul(max_parts_per_segment);
295        let current = if current_total == 0 {
296            0
297        } else if current_total < max_parts_per_segment {
298            let scaled =
299                (current_done as f64) * (max_parts_per_segment as f64 / current_total as f64);
300            scaled.floor() as usize
301        } else {
302            current_done
303        };
304        (completed.saturating_add(current).min(total), total)
305    }
306
307    fn resource_hash_key(hash: &[u8]) -> Option<[u8; 32]> {
308        let mut key = [0u8; 32];
309        if hash.len() != key.len() {
310            return None;
311        }
312        key.copy_from_slice(hash);
313        Some(key)
314    }
315
316    fn incoming_storage(
317        mode: &ResourceReceiveMode,
318        original_hash: &[u8; 32],
319    ) -> std::io::Result<IncomingSplitStorage> {
320        match mode {
321            ResourceReceiveMode::Memory { .. } => Ok(IncomingSplitStorage::Memory(Vec::new())),
322            ResourceReceiveMode::TemporaryFile { directory, .. } => {
323                let (file, path) = crate::resource::create_receive_file(directory, original_hash)?;
324                Ok(IncomingSplitStorage::File {
325                    file,
326                    path,
327                    size: 0,
328                })
329            }
330        }
331    }
332
333    fn incoming_split_progress(split: &IncomingSplitTransfer, sdu: usize) -> (usize, usize) {
334        Self::split_progress_parts(
335            split.current_segment_index,
336            split.total_segments,
337            split.current_received_parts,
338            split.current_total_parts,
339            sdu,
340        )
341    }
342
343    fn outgoing_split_progress(split: &OutgoingSplitTransfer, sdu: usize) -> (usize, usize) {
344        Self::split_progress_parts(
345            split.current_segment_index,
346            split.total_segments,
347            split.current_sent_parts,
348            split.current_total_parts,
349            sdu,
350        )
351    }
352
353    /// Create a new empty link manager.
354    pub fn new() -> Self {
355        LinkManager {
356            receive_worker: None,
357            receive_worker_wake: None,
358            links: HashMap::new(),
359            link_destinations: HashMap::new(),
360            request_handlers: Vec::new(),
361            deferred_request_handlers: Vec::new(),
362            management_paths: Vec::new(),
363            link_mtu_discovery: true,
364        }
365    }
366
367    pub(crate) fn set_link_mtu_discovery(&mut self, enabled: bool) {
368        self.link_mtu_discovery = enabled;
369    }
370
371    /// Register a path hash as a management path.
372    /// Management requests are returned as ManagementRequest actions
373    /// for the driver to handle (since they need access to engine state).
374    pub fn register_management_path(&mut self, path_hash: [u8; 16]) {
375        if !self.management_paths.contains(&path_hash) {
376            self.management_paths.push(path_hash);
377        }
378    }
379
380    pub(crate) fn resource_generation(&self, id: &LinkId) -> Option<std::sync::Arc<()>> {
381        self.links
382            .get(id)
383            .filter(|link| link.engine.state() == LinkState::Active)
384            .map(|link| link.resource_generation.clone())
385    }
386
387    /// Get the derived session key for a link (needed for hole-punch token derivation).
388    pub fn get_derived_key(&self, link_id: &LinkId) -> Option<Vec<u8>> {
389        self.links
390            .get(link_id)
391            .and_then(|link| link.engine.derived_key().map(|dk| dk.to_vec()))
392    }
393
394    /// Return the identity hash learned from a successful LINKIDENTIFY exchange.
395    pub fn remote_identity_hash(&self, link_id: &LinkId) -> Option<[u8; 16]> {
396        self.links
397            .get(link_id)
398            .and_then(|link| link.remote_identity.as_ref().map(|(hash, _)| *hash))
399    }
400
401    /// Return best-known routing hint for link packets.
402    pub fn get_link_route_hint(&self, link_id: &LinkId) -> Option<LinkRouteHint> {
403        self.links.get(link_id).and_then(|link| {
404            link.route_interface.map(|interface| LinkRouteHint {
405                interface,
406                transport_id: link.route_transport_id,
407            })
408        })
409    }
410
411    /// Set the best-known outbound route for a link.
412    pub fn set_link_route_hint(
413        &mut self,
414        link_id: &LinkId,
415        interface: rns_core::transport::types::InterfaceId,
416        transport_id: Option<[u8; 16]>,
417    ) -> bool {
418        let Some(link) = self.links.get_mut(link_id) else {
419            return false;
420        };
421        link.route_interface = Some(interface);
422        link.route_transport_id = transport_id;
423        true
424    }
425
426    /// Register a destination that can accept incoming links.
427    pub fn register_link_destination(
428        &mut self,
429        dest_hash: [u8; 16],
430        sig_prv: Ed25519PrivateKey,
431        sig_pub_bytes: [u8; 32],
432        resource_strategy: ResourceStrategy,
433    ) {
434        self.link_destinations.insert(
435            dest_hash,
436            LinkDestination {
437                sig_prv,
438                sig_pub_bytes,
439                resource_strategy,
440                max_request_size: None,
441            },
442        );
443    }
444
445    /// Configure the maximum request size for this destination.
446    ///
447    /// The new limit also applies to responder links that are already active.
448    /// Returns false when the destination is not registered.
449    pub fn set_link_destination_max_request_size(
450        &mut self,
451        dest_hash: &[u8; 16],
452        max_request_size: Option<usize>,
453    ) -> bool {
454        let Some(destination) = self.link_destinations.get_mut(dest_hash) else {
455            return false;
456        };
457        destination.max_request_size = max_request_size;
458        for link in self.links.values_mut() {
459            if link.dest_hash == *dest_hash && link.dest_sig_pub_bytes.is_none() {
460                link.max_request_size = max_request_size;
461            }
462        }
463        true
464    }
465
466    /// Deregister a link destination.
467    pub fn deregister_link_destination(&mut self, dest_hash: &[u8; 16]) {
468        self.link_destinations.remove(dest_hash);
469    }
470
471    /// Register a request handler for a given path.
472    ///
473    /// `path`: the request path string (e.g. "/status")
474    /// `allowed_list`: None = allow all, Some(list) = restrict to these identity hashes
475    /// `handler`: called with (link_id, path, request_data, remote_identity) -> Option<response>
476    pub fn register_request_handler<F>(
477        &mut self,
478        path: &str,
479        allowed_list: Option<Vec<[u8; 16]>>,
480        handler: F,
481    ) where
482        F: Fn(LinkId, &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<Vec<u8>>
483            + Send
484            + 'static,
485    {
486        let path_hash = compute_path_hash(path);
487        self.request_handlers.push(RequestHandlerEntry {
488            path: path.to_string(),
489            path_hash,
490            allowed_list,
491            handler: Box::new(move |link_id, p, data, remote| {
492                handler(link_id, p, data, remote).map(RequestResponse::Bytes)
493            }),
494        });
495    }
496
497    /// Register a request handler that can return resource responses with metadata.
498    pub fn register_request_handler_response<F>(
499        &mut self,
500        path: &str,
501        allowed_list: Option<Vec<[u8; 16]>>,
502        handler: F,
503    ) where
504        F: Fn(LinkId, &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<RequestResponse>
505            + Send
506            + 'static,
507    {
508        let path_hash = compute_path_hash(path);
509        self.request_handlers.push(RequestHandlerEntry {
510            path: path.to_string(),
511            path_hash,
512            allowed_list,
513            handler: Box::new(handler),
514        });
515    }
516
517    /// Register a request handler that produces its response asynchronously.
518    pub fn register_deferred_request_handler<F>(
519        &mut self,
520        path: &str,
521        allowed_list: Option<Vec<[u8; 16]>>,
522        handler: F,
523    ) where
524        F: Fn(LinkId, &str, [u8; 16], &[u8], Option<&([u8; 16], [u8; 64])>) + Send + 'static,
525    {
526        self.deferred_request_handlers
527            .push(DeferredRequestHandlerEntry {
528                path: path.to_string(),
529                path_hash: compute_path_hash(path),
530                allowed_list,
531                handler: Box::new(handler),
532            });
533    }
534
535    /// Create an outbound link to a destination.
536    ///
537    /// `dest_sig_pub_bytes` is the destination's Ed25519 signing public key
538    /// (needed to verify LRPROOF). In Python this comes from the Destination's Identity.
539    ///
540    /// Returns `(link_id, actions)`. The first action will be a SendPacket with
541    /// the LINKREQUEST.
542    pub fn create_link(
543        &mut self,
544        dest_hash: &[u8; 16],
545        dest_sig_pub_bytes: &[u8; 32],
546        hops: u8,
547        mtu: u32,
548        rng: &mut dyn Rng,
549    ) -> (LinkId, Vec<LinkManagerAction>) {
550        let mode = LinkMode::Aes256Cbc;
551        let signalled_mtu = self.link_mtu_discovery.then_some(mtu);
552        let (mut engine, request_data) =
553            LinkEngine::new_initiator(dest_hash, hops, mode, signalled_mtu, time::now(), rng);
554
555        // Build the LINKREQUEST packet to compute link_id
556        let flags = PacketFlags {
557            header_type: constants::HEADER_1,
558            context_flag: constants::FLAG_UNSET,
559            transport_type: constants::TRANSPORT_BROADCAST,
560            destination_type: constants::DESTINATION_SINGLE,
561            packet_type: constants::PACKET_TYPE_LINKREQUEST,
562        };
563
564        let packet = match RawPacket::pack(
565            flags,
566            0,
567            dest_hash,
568            None,
569            constants::CONTEXT_NONE,
570            &request_data,
571        ) {
572            Ok(p) => p,
573            Err(_) => {
574                // Should not happen with valid data
575                return ([0u8; 16], Vec::new());
576            }
577        };
578
579        engine.set_link_id_from_hashable(&packet.get_hashable_part(), request_data.len());
580        let link_id = *engine.link_id();
581
582        engine.record_outbound_traffic(packet.data().len());
583        let managed = ManagedLink {
584            resource_generation: std::sync::Arc::new(()),
585            engine,
586            channel: None,
587            pre_rtt_packets: Vec::new(),
588            pending_channel_packets: HashMap::new(),
589            channel_send_ok: 0,
590            channel_send_not_ready: 0,
591            channel_send_too_big: 0,
592            channel_send_other_error: 0,
593            channel_messages_received: 0,
594            channel_proofs_sent: 0,
595            channel_proofs_received: 0,
596            dest_hash: *dest_hash,
597            remote_identity: None,
598            dest_sig_pub_bytes: Some(*dest_sig_pub_bytes),
599            incoming_resources: Vec::new(),
600            outgoing_resources: Vec::new(),
601            pending_requests: HashMap::new(),
602            incoming_splits: HashMap::new(),
603            outgoing_splits: HashMap::new(),
604            outgoing_streams: HashMap::new(),
605            resource_strategy: ResourceStrategy::default(),
606            resource_receive_mode: ResourceReceiveMode::default(),
607            max_request_size: None,
608            route_interface: None,
609            route_transport_id: None,
610        };
611        self.links.insert(link_id, managed);
612
613        let actions = vec![
614            // Register the link_id as a local destination so we can receive LRPROOF
615            LinkManagerAction::RegisterLinkDest { link_id },
616            // Send the LINKREQUEST packet
617            LinkManagerAction::SendPacket {
618                raw: packet.raw,
619                dest_type: constants::DESTINATION_LINK,
620                attached_interface: None,
621            },
622        ];
623
624        (link_id, actions)
625    }
626
627    /// Handle a packet delivered locally (via DeliverLocal).
628    ///
629    /// Returns actions for the driver to dispatch. The `dest_hash` is the
630    /// packet's destination_hash field. `raw` is the full packet bytes.
631    /// `packet_hash` is the SHA-256 hash.
632    pub fn handle_local_delivery(
633        &mut self,
634        dest_hash: [u8; 16],
635        raw: &[u8],
636        packet_hash: [u8; 32],
637        receiving_interface: rns_core::transport::types::InterfaceId,
638        rng: &mut dyn Rng,
639    ) -> Vec<LinkManagerAction> {
640        let packet = match RawPacket::unpack(raw) {
641            Ok(p) => p,
642            Err(_) => return Vec::new(),
643        };
644
645        // Python accounts link traffic once the packet is accepted on its
646        // attached interface, before dispatch/decryption of its context.
647        if packet.flags.packet_type != constants::PACKET_TYPE_LINKREQUEST {
648            if let Some(link) = self.links.get_mut(&dest_hash) {
649                link.engine.record_inbound_traffic(packet.data().len());
650            }
651        }
652
653        match packet.flags.packet_type {
654            constants::PACKET_TYPE_LINKREQUEST => {
655                self.handle_linkrequest(&dest_hash, &packet, receiving_interface, rng)
656            }
657            constants::PACKET_TYPE_PROOF if packet.context == constants::CONTEXT_LRPROOF => {
658                // LRPROOF: dest_hash is the link_id
659                self.handle_lrproof(&dest_hash, &packet, receiving_interface, rng)
660            }
661            constants::PACKET_TYPE_PROOF if packet.context == constants::CONTEXT_RESOURCE_PRF => {
662                // Resource proofs are PROOF packets with raw proof data, not
663                // encrypted DATA packets.
664                self.handle_resource_prf(&dest_hash, packet.data(), rng)
665            }
666            constants::PACKET_TYPE_PROOF => self.handle_link_proof(&dest_hash, &packet, rng),
667            constants::PACKET_TYPE_DATA => {
668                self.handle_link_data(&dest_hash, &packet, packet_hash, receiving_interface, rng)
669            }
670            _ => Vec::new(),
671        }
672    }
673
674    /// Handle an incoming LINKREQUEST packet.
675    fn handle_linkrequest(
676        &mut self,
677        dest_hash: &[u8; 16],
678        packet: &RawPacket,
679        receiving_interface: rns_core::transport::types::InterfaceId,
680        rng: &mut dyn Rng,
681    ) -> Vec<LinkManagerAction> {
682        // Look up the link destination
683        let ld = match self.link_destinations.get(dest_hash) {
684            Some(ld) => ld,
685            None => return Vec::new(),
686        };
687
688        let hashable = packet.get_hashable_part();
689        let now = time::now();
690
691        // Create responder engine
692        let (engine, lrproof_data) = match LinkEngine::new_responder(
693            &ld.sig_prv,
694            &ld.sig_pub_bytes,
695            packet.data(),
696            &hashable,
697            dest_hash,
698            packet.hops,
699            now,
700            rng,
701        ) {
702            Ok(r) => r,
703            Err(e) => {
704                log::debug!("LINKREQUEST rejected: {}", e);
705                return vec![LinkManagerAction::ProtocolViolation {
706                    receiving_interface,
707                }];
708            }
709        };
710
711        let link_id = *engine.link_id();
712        log::debug!(
713            "LINKREQUEST accepted: link={:02x?} iface={} header_type={} transport_id_present={} hops={}",
714            &link_id[..4],
715            receiving_interface.0,
716            packet.flags.header_type,
717            packet.transport_id.is_some(),
718            packet.hops
719        );
720
721        let managed = ManagedLink {
722            resource_generation: std::sync::Arc::new(()),
723            engine,
724            channel: None,
725            pre_rtt_packets: Vec::new(),
726            pending_channel_packets: HashMap::new(),
727            channel_send_ok: 0,
728            channel_send_not_ready: 0,
729            channel_send_too_big: 0,
730            channel_send_other_error: 0,
731            channel_messages_received: 0,
732            channel_proofs_sent: 0,
733            channel_proofs_received: 0,
734            dest_hash: *dest_hash,
735            remote_identity: None,
736            dest_sig_pub_bytes: None,
737            incoming_resources: Vec::new(),
738            outgoing_resources: Vec::new(),
739            pending_requests: HashMap::new(),
740            incoming_splits: HashMap::new(),
741            outgoing_splits: HashMap::new(),
742            outgoing_streams: HashMap::new(),
743            resource_strategy: ld.resource_strategy,
744            resource_receive_mode: ResourceReceiveMode::default(),
745            max_request_size: ld.max_request_size,
746            route_interface: Some(receiving_interface),
747            route_transport_id: if packet.flags.header_type == constants::HEADER_2 {
748                packet.transport_id
749            } else {
750                None
751            },
752        };
753        self.links.insert(link_id, managed);
754
755        // Build LRPROOF packet: type=PROOF, context=LRPROOF, dest=link_id
756        let flags = PacketFlags {
757            header_type: constants::HEADER_1,
758            context_flag: constants::FLAG_UNSET,
759            transport_type: constants::TRANSPORT_BROADCAST,
760            destination_type: constants::DESTINATION_LINK,
761            packet_type: constants::PACKET_TYPE_PROOF,
762        };
763
764        let mut actions = Vec::new();
765
766        // Register link_id as local destination so we receive link data
767        actions.push(LinkManagerAction::RegisterLinkDest { link_id });
768
769        if let Ok((raw, _packet_hash)) = RawPacket::pack_raw_with_hash(
770            flags,
771            0,
772            &link_id,
773            None,
774            constants::CONTEXT_LRPROOF,
775            &lrproof_data,
776        ) {
777            log::debug!(
778                "LRPROOF queued: link={:02x?} route_iface={} route_tid_present={} hops=0",
779                &link_id[..4],
780                receiving_interface.0,
781                packet.transport_id.is_some()
782            );
783            actions.push(LinkManagerAction::SendPacket {
784                raw,
785                dest_type: constants::DESTINATION_LINK,
786                attached_interface: None,
787            });
788        }
789
790        // Notify hook system about the incoming link request
791        actions.push(LinkManagerAction::LinkRequestReceived {
792            link_id,
793            receiving_interface,
794        });
795
796        actions
797    }
798
799    fn handle_link_proof(
800        &mut self,
801        link_id: &LinkId,
802        packet: &RawPacket,
803        rng: &mut dyn Rng,
804    ) -> Vec<LinkManagerAction> {
805        if packet.data().len() != 96 {
806            return Vec::new();
807        }
808
809        let mut tracked_hash = [0u8; 32];
810        tracked_hash.copy_from_slice(&packet.data()[..32]);
811
812        let mut signature = [0u8; 64];
813        signature.copy_from_slice(&packet.data()[32..]);
814
815        let Some(link) = self.links.get_mut(link_id) else {
816            return Vec::new();
817        };
818        if !link.engine.validate_packet_proof(&tracked_hash, &signature) {
819            return Vec::new();
820        }
821        let Some(sequence) = link.pending_channel_packets.remove(&tracked_hash) else {
822            return Vec::new();
823        };
824        link.channel_proofs_received += 1;
825        let Some(channel) = link.channel.as_mut() else {
826            return Vec::new();
827        };
828
829        let chan_actions = channel.packet_delivered(sequence);
830        let _ = channel;
831        let _ = link;
832        self.process_channel_actions(link_id, chan_actions, rng)
833    }
834
835    fn build_link_packet_proof(
836        &mut self,
837        link_id: &LinkId,
838        packet_hash: &[u8; 32],
839    ) -> Vec<LinkManagerAction> {
840        let signature = match self.links.get_mut(link_id) {
841            Some(link) => {
842                link.channel_proofs_sent += 1;
843                link.engine.sign_packet_hash(packet_hash)
844            }
845            None => return Vec::new(),
846        };
847        let mut proof_data = Vec::with_capacity(96);
848        proof_data.extend_from_slice(packet_hash);
849        proof_data.extend_from_slice(&signature);
850
851        let flags = PacketFlags {
852            header_type: constants::HEADER_1,
853            context_flag: constants::FLAG_UNSET,
854            transport_type: constants::TRANSPORT_BROADCAST,
855            destination_type: constants::DESTINATION_LINK,
856            packet_type: constants::PACKET_TYPE_PROOF,
857        };
858        if let Ok((raw, _packet_hash)) = RawPacket::pack_raw_with_hash(
859            flags,
860            0,
861            link_id,
862            None,
863            constants::CONTEXT_NONE,
864            &proof_data,
865        ) {
866            vec![LinkManagerAction::SendPacket {
867                raw,
868                dest_type: constants::DESTINATION_LINK,
869                attached_interface: None,
870            }]
871        } else {
872            Vec::new()
873        }
874    }
875
876    /// Handle an incoming LRPROOF packet (initiator side).
877    fn handle_lrproof(
878        &mut self,
879        link_id_bytes: &[u8; 16],
880        packet: &RawPacket,
881        receiving_interface: rns_core::transport::types::InterfaceId,
882        rng: &mut dyn Rng,
883    ) -> Vec<LinkManagerAction> {
884        let link = match self.links.get_mut(link_id_bytes) {
885            Some(l) => l,
886            None => return Vec::new(),
887        };
888
889        link.route_interface = Some(receiving_interface);
890        if packet.flags.header_type == constants::HEADER_2 {
891            if let Some(transport_id) = packet.transport_id {
892                link.route_transport_id = Some(transport_id);
893            }
894        }
895        log::debug!(
896            "LRPROOF received: link={:02x?} iface={} header_type={} transport_id_present={}",
897            &link_id_bytes[..4],
898            receiving_interface.0,
899            packet.flags.header_type,
900            packet.transport_id.is_some()
901        );
902
903        if link.engine.state() != LinkState::Pending || !link.engine.is_initiator() {
904            return Vec::new();
905        }
906
907        // The destination's signing pub key was stored when create_link was called
908        let dest_sig_pub_bytes = match link.dest_sig_pub_bytes {
909            Some(b) => b,
910            None => {
911                log::debug!("LRPROOF: no destination signing key available");
912                return Vec::new();
913            }
914        };
915
916        let now = time::now();
917        let (lrrtt_encrypted, link_actions) = match link.engine.handle_lrproof_with_hops(
918            packet.data(),
919            &dest_sig_pub_bytes,
920            Some(packet.hops),
921            now,
922            rng,
923        ) {
924            Ok(r) => r,
925            Err(e) => {
926                log::debug!("LRPROOF validation failed: {}", e);
927                return Vec::new();
928            }
929        };
930
931        let link_id = *link.engine.link_id();
932        let mut actions = Vec::new();
933
934        // Process link actions (StateChanged, LinkEstablished)
935        actions.extend(self.process_link_actions(&link_id, &link_actions));
936
937        // Send LRRTT: type=DATA, context=LRRTT, dest=link_id
938        let flags = PacketFlags {
939            header_type: constants::HEADER_1,
940            context_flag: constants::FLAG_UNSET,
941            transport_type: constants::TRANSPORT_BROADCAST,
942            destination_type: constants::DESTINATION_LINK,
943            packet_type: constants::PACKET_TYPE_DATA,
944        };
945
946        if let Ok((raw, _packet_hash)) = RawPacket::pack_raw_with_hash(
947            flags,
948            0,
949            &link_id,
950            None,
951            constants::CONTEXT_LRRTT,
952            &lrrtt_encrypted,
953        ) {
954            actions.push(LinkManagerAction::SendPacket {
955                raw,
956                dest_type: constants::DESTINATION_LINK,
957                attached_interface: None,
958            });
959        }
960
961        // Initialize channel now that link is active
962        if let Some(link) = self.links.get_mut(&link_id) {
963            if link.engine.state() == LinkState::Active {
964                let rtt = link.engine.rtt().unwrap_or(1.0);
965                link.channel = Some(Channel::new(rtt));
966            }
967        }
968
969        actions
970    }
971
972    /// Handle DATA packets on an established link.
973    ///
974    /// Structured to avoid borrow checker issues: we perform engine operations
975    /// on the link, collect intermediate results, drop the mutable borrow, then
976    /// call self methods that need immutable access.
977    fn handle_link_data(
978        &mut self,
979        link_id_bytes: &[u8; 16],
980        packet: &RawPacket,
981        packet_hash: [u8; 32],
982        receiving_interface: rns_core::transport::types::InterfaceId,
983        rng: &mut dyn Rng,
984    ) -> Vec<LinkManagerAction> {
985        // First pass: perform engine operations, collect results
986        enum LinkDataResult<'a> {
987            Lrrtt {
988                link_id: LinkId,
989                link_actions: Vec<LinkAction>,
990            },
991            Identify {
992                link_id: LinkId,
993                link_actions: Vec<LinkAction>,
994            },
995            Keepalive {
996                link_id: LinkId,
997                inbound_actions: Vec<LinkAction>,
998                reply: bool,
999                received_at: f64,
1000            },
1001            LinkClose {
1002                link_id: LinkId,
1003                teardown_actions: Vec<LinkAction>,
1004            },
1005            Channel {
1006                link_id: LinkId,
1007                inbound_actions: Vec<LinkAction>,
1008                plaintext: Vec<u8>,
1009                packet_hash: [u8; 32],
1010            },
1011            Request {
1012                link_id: LinkId,
1013                inbound_actions: Vec<LinkAction>,
1014                plaintext: Vec<u8>,
1015                request_id: [u8; 16],
1016            },
1017            Response {
1018                link_id: LinkId,
1019                inbound_actions: Vec<LinkAction>,
1020                plaintext: Vec<u8>,
1021            },
1022            Generic {
1023                link_id: LinkId,
1024                inbound_actions: Vec<LinkAction>,
1025                plaintext: Vec<u8>,
1026                context: u8,
1027                packet_hash: [u8; 32],
1028            },
1029            /// Resource advertisement (link-decrypted).
1030            ResourceAdv {
1031                link_id: LinkId,
1032                inbound_actions: Vec<LinkAction>,
1033                plaintext: Vec<u8>,
1034            },
1035            /// Resource part request (link-decrypted).
1036            ResourceReq {
1037                link_id: LinkId,
1038                inbound_actions: Vec<LinkAction>,
1039                plaintext: Vec<u8>,
1040            },
1041            /// Resource hashmap update (link-decrypted).
1042            ResourceHmu {
1043                link_id: LinkId,
1044                inbound_actions: Vec<LinkAction>,
1045                plaintext: Vec<u8>,
1046            },
1047            /// Resource part data (NOT link-decrypted; parts are pre-encrypted by ResourceSender).
1048            ResourcePart {
1049                link_id: LinkId,
1050                inbound_actions: Vec<LinkAction>,
1051                raw_data: &'a [u8],
1052            },
1053            /// Resource proof (feed to sender).
1054            ResourcePrf {
1055                link_id: LinkId,
1056                inbound_actions: Vec<LinkAction>,
1057                plaintext: Vec<u8>,
1058            },
1059            /// Resource cancel from initiator (link-decrypted).
1060            ResourceIcl {
1061                link_id: LinkId,
1062                inbound_actions: Vec<LinkAction>,
1063                resource_hash: Vec<u8>,
1064            },
1065            /// Resource cancel from receiver (link-decrypted).
1066            ResourceRcl {
1067                link_id: LinkId,
1068                inbound_actions: Vec<LinkAction>,
1069                resource_hash: Vec<u8>,
1070            },
1071            Error,
1072        }
1073
1074        let result = {
1075            let link = match self.links.get_mut(link_id_bytes) {
1076                Some(l) => l,
1077                None => return Vec::new(),
1078            };
1079
1080            // Python exposes ACTIVE to its utility thread before the proof thread
1081            // sends LRRTT. Preserve early identity/request ordering, but do not
1082            // invoke application handlers until the responder handshake completes.
1083            if !link.engine.is_initiator()
1084                && link.engine.state() == LinkState::Handshake
1085                && matches!(
1086                    packet.context,
1087                    constants::CONTEXT_LINKIDENTIFY | constants::CONTEXT_REQUEST
1088                )
1089            {
1090                let packet_bytes = packet.raw.len() + packet.data().len();
1091                let queued_bytes: usize = link
1092                    .pre_rtt_packets
1093                    .iter()
1094                    .map(|pending| pending.packet.raw.len() + pending.packet.data().len())
1095                    .sum();
1096                if link.pre_rtt_packets.len() < MAX_PRE_RTT_PACKETS
1097                    && packet_bytes <= MAX_PRE_RTT_BYTES.saturating_sub(queued_bytes)
1098                    && link.engine.decrypt(packet.data()).is_ok()
1099                {
1100                    link.pre_rtt_packets.push(PreRttPacket {
1101                        packet: packet.clone(),
1102                        packet_hash,
1103                        receiving_interface,
1104                    });
1105                }
1106                return Vec::new();
1107            }
1108
1109            link.route_interface = Some(receiving_interface);
1110            if packet.flags.header_type == constants::HEADER_2 {
1111                if let Some(transport_id) = packet.transport_id {
1112                    link.route_transport_id = Some(transport_id);
1113                }
1114            } else {
1115                link.route_transport_id = None;
1116            }
1117
1118            match packet.context {
1119                constants::CONTEXT_LRRTT => {
1120                    match link.engine.handle_lrrtt_with_hops(
1121                        packet.data(),
1122                        Some(packet.hops),
1123                        time::now(),
1124                    ) {
1125                        Ok(link_actions) => {
1126                            let link_id = *link.engine.link_id();
1127                            LinkDataResult::Lrrtt {
1128                                link_id,
1129                                link_actions,
1130                            }
1131                        }
1132                        Err(e) => {
1133                            log::debug!("LRRTT handling failed: {}", e);
1134                            LinkDataResult::Error
1135                        }
1136                    }
1137                }
1138                constants::CONTEXT_LINKIDENTIFY => {
1139                    match link.engine.handle_identify(packet.data()) {
1140                        Ok(link_actions) => {
1141                            let link_id = *link.engine.link_id();
1142                            link.remote_identity = link.engine.remote_identity().cloned();
1143                            LinkDataResult::Identify {
1144                                link_id,
1145                                link_actions,
1146                            }
1147                        }
1148                        Err(e) => {
1149                            log::debug!("LINKIDENTIFY failed: {}", e);
1150                            LinkDataResult::Error
1151                        }
1152                    }
1153                }
1154                constants::CONTEXT_KEEPALIVE => {
1155                    let received_at = time::now();
1156                    let reply = link
1157                        .engine
1158                        .should_reply_keepalive(packet.data(), received_at);
1159                    let inbound_actions = link.engine.record_inbound(received_at);
1160                    let link_id = *link.engine.link_id();
1161                    LinkDataResult::Keepalive {
1162                        link_id,
1163                        inbound_actions,
1164                        reply,
1165                        received_at,
1166                    }
1167                }
1168                constants::CONTEXT_LINKCLOSE => match link.engine.decrypt(packet.data()) {
1169                    Ok(plaintext) if plaintext.as_slice() == link_id_bytes => {
1170                        let teardown_actions = link.engine.handle_teardown();
1171                        let link_id = *link.engine.link_id();
1172                        LinkDataResult::LinkClose {
1173                            link_id,
1174                            teardown_actions,
1175                        }
1176                    }
1177                    _ => LinkDataResult::Error,
1178                },
1179                constants::CONTEXT_CHANNEL => match link.engine.decrypt(packet.data()) {
1180                    Ok(plaintext) => {
1181                        let inbound_actions = link.engine.record_inbound(time::now());
1182                        let link_id = *link.engine.link_id();
1183                        LinkDataResult::Channel {
1184                            link_id,
1185                            inbound_actions,
1186                            plaintext,
1187                            packet_hash,
1188                        }
1189                    }
1190                    Err(_) => LinkDataResult::Error,
1191                },
1192                constants::CONTEXT_REQUEST => match link.engine.decrypt(packet.data()) {
1193                    Ok(plaintext) => {
1194                        let inbound_actions = link.engine.record_inbound(time::now());
1195                        let link_id = *link.engine.link_id();
1196                        let request_id = packet.get_truncated_hash();
1197                        LinkDataResult::Request {
1198                            link_id,
1199                            inbound_actions,
1200                            plaintext,
1201                            request_id,
1202                        }
1203                    }
1204                    Err(_) => LinkDataResult::Error,
1205                },
1206                constants::CONTEXT_RESPONSE => match link.engine.decrypt(packet.data()) {
1207                    Ok(plaintext) => {
1208                        let inbound_actions = link.engine.record_inbound(time::now());
1209                        let link_id = *link.engine.link_id();
1210                        LinkDataResult::Response {
1211                            link_id,
1212                            inbound_actions,
1213                            plaintext,
1214                        }
1215                    }
1216                    Err(_) => LinkDataResult::Error,
1217                },
1218                // --- Resource contexts ---
1219                constants::CONTEXT_RESOURCE_ADV => match link.engine.decrypt(packet.data()) {
1220                    Ok(plaintext) => {
1221                        let inbound_actions = link.engine.record_inbound(time::now());
1222                        let link_id = *link.engine.link_id();
1223                        LinkDataResult::ResourceAdv {
1224                            link_id,
1225                            inbound_actions,
1226                            plaintext,
1227                        }
1228                    }
1229                    Err(_) => LinkDataResult::Error,
1230                },
1231                constants::CONTEXT_RESOURCE_REQ => match link.engine.decrypt(packet.data()) {
1232                    Ok(plaintext) => {
1233                        let inbound_actions = link.engine.record_inbound(time::now());
1234                        let link_id = *link.engine.link_id();
1235                        LinkDataResult::ResourceReq {
1236                            link_id,
1237                            inbound_actions,
1238                            plaintext,
1239                        }
1240                    }
1241                    Err(_) => LinkDataResult::Error,
1242                },
1243                constants::CONTEXT_RESOURCE_HMU => match link.engine.decrypt(packet.data()) {
1244                    Ok(plaintext) => {
1245                        let inbound_actions = link.engine.record_inbound(time::now());
1246                        let link_id = *link.engine.link_id();
1247                        LinkDataResult::ResourceHmu {
1248                            link_id,
1249                            inbound_actions,
1250                            plaintext,
1251                        }
1252                    }
1253                    Err(_) => LinkDataResult::Error,
1254                },
1255                constants::CONTEXT_RESOURCE => {
1256                    // Resource parts are NOT link-decrypted — they're pre-encrypted by ResourceSender
1257                    let inbound_actions = link.engine.record_inbound(time::now());
1258                    let link_id = *link.engine.link_id();
1259                    LinkDataResult::ResourcePart {
1260                        link_id,
1261                        inbound_actions,
1262                        raw_data: packet.data(),
1263                    }
1264                }
1265                constants::CONTEXT_RESOURCE_PRF => match link.engine.decrypt(packet.data()) {
1266                    Ok(plaintext) => {
1267                        let inbound_actions = link.engine.record_inbound(time::now());
1268                        let link_id = *link.engine.link_id();
1269                        LinkDataResult::ResourcePrf {
1270                            link_id,
1271                            inbound_actions,
1272                            plaintext,
1273                        }
1274                    }
1275                    Err(_) => LinkDataResult::Error,
1276                },
1277                constants::CONTEXT_RESOURCE_ICL => match link.engine.decrypt(packet.data()) {
1278                    Ok(resource_hash) if resource_hash.len() == 32 => {
1279                        let inbound_actions = link.engine.record_inbound(time::now());
1280                        let link_id = *link.engine.link_id();
1281                        LinkDataResult::ResourceIcl {
1282                            link_id,
1283                            inbound_actions,
1284                            resource_hash,
1285                        }
1286                    }
1287                    _ => LinkDataResult::Error,
1288                },
1289                constants::CONTEXT_RESOURCE_RCL => match link.engine.decrypt(packet.data()) {
1290                    Ok(resource_hash) if resource_hash.len() == 32 => {
1291                        let inbound_actions = link.engine.record_inbound(time::now());
1292                        let link_id = *link.engine.link_id();
1293                        LinkDataResult::ResourceRcl {
1294                            link_id,
1295                            inbound_actions,
1296                            resource_hash,
1297                        }
1298                    }
1299                    _ => LinkDataResult::Error,
1300                },
1301                _ => match link.engine.decrypt(packet.data()) {
1302                    Ok(plaintext) => {
1303                        let inbound_actions = link.engine.record_inbound(time::now());
1304                        let link_id = *link.engine.link_id();
1305                        LinkDataResult::Generic {
1306                            link_id,
1307                            inbound_actions,
1308                            plaintext,
1309                            context: packet.context,
1310                            packet_hash,
1311                        }
1312                    }
1313                    Err(_) => LinkDataResult::Error,
1314                },
1315            }
1316        }; // mutable borrow of self.links dropped here
1317
1318        // Second pass: process results using self methods
1319        let mut actions = Vec::new();
1320        match result {
1321            LinkDataResult::Lrrtt {
1322                link_id,
1323                link_actions,
1324            } => {
1325                actions.extend(self.process_link_actions(&link_id, &link_actions));
1326                // Initialize channel
1327                if let Some(link) = self.links.get_mut(&link_id) {
1328                    if link.engine.state() == LinkState::Active {
1329                        let rtt = link.engine.rtt().unwrap_or(1.0);
1330                        link.channel = Some(Channel::new(rtt));
1331                    }
1332                }
1333                let pending = self
1334                    .links
1335                    .get_mut(&link_id)
1336                    .map(|link| std::mem::take(&mut link.pre_rtt_packets))
1337                    .unwrap_or_default();
1338                for pending in pending {
1339                    actions.extend(self.handle_link_data(
1340                        &link_id,
1341                        &pending.packet,
1342                        pending.packet_hash,
1343                        pending.receiving_interface,
1344                        rng,
1345                    ));
1346                }
1347            }
1348            LinkDataResult::Identify {
1349                link_id,
1350                link_actions,
1351            } => {
1352                actions.extend(self.process_link_actions(&link_id, &link_actions));
1353            }
1354            LinkDataResult::Keepalive {
1355                link_id,
1356                inbound_actions,
1357                reply,
1358                received_at,
1359            } => {
1360                log::debug!(
1361                    "Link keepalive received: link={:02x?} reply={reply}",
1362                    &link_id[..4]
1363                );
1364                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1365                if reply {
1366                    let flags = PacketFlags {
1367                        header_type: constants::HEADER_1,
1368                        context_flag: constants::FLAG_UNSET,
1369                        transport_type: constants::TRANSPORT_BROADCAST,
1370                        destination_type: constants::DESTINATION_LINK,
1371                        packet_type: constants::PACKET_TYPE_DATA,
1372                    };
1373                    if let Ok((raw, _)) = RawPacket::pack_raw_with_hash(
1374                        flags,
1375                        0,
1376                        &link_id,
1377                        None,
1378                        constants::CONTEXT_KEEPALIVE,
1379                        &[0xfe],
1380                    ) {
1381                        actions.push(LinkManagerAction::SendPacket {
1382                            raw,
1383                            dest_type: constants::DESTINATION_LINK,
1384                            attached_interface: None,
1385                        });
1386                        if let Some(link) = self.links.get_mut(&link_id) {
1387                            link.engine.record_outbound(received_at, true);
1388                        }
1389                    }
1390                }
1391            }
1392            LinkDataResult::LinkClose {
1393                link_id,
1394                teardown_actions,
1395            } => {
1396                actions.extend(self.process_link_actions(&link_id, &teardown_actions));
1397            }
1398            LinkDataResult::Channel {
1399                link_id,
1400                inbound_actions,
1401                plaintext,
1402                packet_hash,
1403            } => {
1404                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1405                // Feed plaintext to channel
1406                if let Some(link) = self.links.get_mut(&link_id) {
1407                    if let Some(ref mut channel) = link.channel {
1408                        let chan_actions = channel.receive(&plaintext, time::now());
1409                        link.channel_messages_received += chan_actions
1410                            .iter()
1411                            .filter(|action| {
1412                                matches!(
1413                                    action,
1414                                    rns_core::channel::ChannelAction::MessageReceived { .. }
1415                                )
1416                            })
1417                            .count()
1418                            as u64;
1419                        // process_channel_actions needs immutable self, so collect first
1420                        let _ = link;
1421                        actions.extend(self.process_channel_actions(&link_id, chan_actions, rng));
1422                    }
1423                }
1424                actions.extend(self.build_link_packet_proof(&link_id, &packet_hash));
1425            }
1426            LinkDataResult::Request {
1427                link_id,
1428                inbound_actions,
1429                plaintext,
1430                request_id,
1431            } => {
1432                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1433                let size_ok = self
1434                    .links
1435                    .get(&link_id)
1436                    .and_then(|link| link.max_request_size)
1437                    .is_none_or(|limit| plaintext.len() <= limit);
1438                if size_ok {
1439                    actions.extend(self.handle_request(&link_id, &plaintext, request_id, rng));
1440                } else {
1441                    log::debug!(
1442                        "ignored request with excessive size {} bytes on link {:02x?}",
1443                        plaintext.len(),
1444                        &link_id[..4]
1445                    );
1446                }
1447            }
1448            LinkDataResult::Response {
1449                link_id,
1450                inbound_actions,
1451                plaintext,
1452            } => {
1453                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1454                // Unpack msgpack response: [Bin(request_id), response_value]
1455                actions.extend(self.handle_response(&link_id, &plaintext, None, None));
1456            }
1457            LinkDataResult::Generic {
1458                link_id,
1459                inbound_actions,
1460                plaintext,
1461                context,
1462                packet_hash,
1463            } => {
1464                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1465                actions.push(LinkManagerAction::LinkDataReceived {
1466                    link_id,
1467                    context,
1468                    data: plaintext,
1469                });
1470
1471                actions.extend(self.build_link_packet_proof(&link_id, &packet_hash));
1472            }
1473            LinkDataResult::ResourceAdv {
1474                link_id,
1475                inbound_actions,
1476                plaintext,
1477            } => {
1478                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1479                actions.extend(self.handle_resource_adv(&link_id, &plaintext, rng));
1480            }
1481            LinkDataResult::ResourceReq {
1482                link_id,
1483                inbound_actions,
1484                plaintext,
1485            } => {
1486                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1487                actions.extend(self.handle_resource_req(&link_id, &plaintext, rng));
1488            }
1489            LinkDataResult::ResourceHmu {
1490                link_id,
1491                inbound_actions,
1492                plaintext,
1493            } => {
1494                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1495                actions.extend(self.handle_resource_hmu(&link_id, &plaintext, rng));
1496            }
1497            LinkDataResult::ResourcePart {
1498                link_id,
1499                inbound_actions,
1500                raw_data,
1501            } => {
1502                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1503                actions.extend(self.handle_resource_part(&link_id, raw_data, rng));
1504            }
1505            LinkDataResult::ResourcePrf {
1506                link_id,
1507                inbound_actions,
1508                plaintext,
1509            } => {
1510                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1511                actions.extend(self.handle_resource_prf(&link_id, &plaintext, rng));
1512            }
1513            LinkDataResult::ResourceIcl {
1514                link_id,
1515                inbound_actions,
1516                resource_hash,
1517            } => {
1518                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1519                actions.extend(self.handle_resource_icl(&link_id, &resource_hash));
1520            }
1521            LinkDataResult::ResourceRcl {
1522                link_id,
1523                inbound_actions,
1524                resource_hash,
1525            } => {
1526                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
1527                actions.extend(self.handle_resource_rcl(&link_id, &resource_hash));
1528            }
1529            LinkDataResult::Error => {}
1530        }
1531
1532        actions
1533    }
1534
1535    pub fn send_on_link(
1536        &self,
1537        link_id: &LinkId,
1538        plaintext: &[u8],
1539        context: u8,
1540        rng: &mut dyn Rng,
1541    ) -> Vec<LinkManagerAction> {
1542        self.try_send_on_link(link_id, plaintext, context, rng)
1543            .unwrap_or_default()
1544    }
1545
1546    /// Prepare a best-effort direct Link packet and report admission failures.
1547    pub fn try_send_on_link(
1548        &self,
1549        link_id: &LinkId,
1550        plaintext: &[u8],
1551        context: u8,
1552        rng: &mut dyn Rng,
1553    ) -> Result<Vec<LinkManagerAction>, crate::event::LinkDatagramError> {
1554        use crate::event::LinkDatagramError;
1555        let link = match self.links.get(link_id) {
1556            Some(l) => l,
1557            None => return Err(LinkDatagramError::LinkNotFound),
1558        };
1559
1560        if link.engine.state() != LinkState::Active {
1561            return Err(LinkDatagramError::LinkNotActive);
1562        }
1563        let maximum = link.engine.mdu();
1564        if plaintext.len() > maximum {
1565            return Err(LinkDatagramError::PayloadTooLarge {
1566                maximum,
1567                actual: plaintext.len(),
1568            });
1569        }
1570
1571        let encrypted = match link.engine.encrypt(plaintext, rng) {
1572            Ok(e) => e,
1573            Err(_) => return Err(LinkDatagramError::EncryptionFailed),
1574        };
1575
1576        let flags = PacketFlags {
1577            header_type: constants::HEADER_1,
1578            context_flag: constants::FLAG_UNSET,
1579            transport_type: constants::TRANSPORT_BROADCAST,
1580            destination_type: constants::DESTINATION_LINK,
1581            packet_type: constants::PACKET_TYPE_DATA,
1582        };
1583
1584        let mut actions = Vec::new();
1585        let (raw, _packet_hash) =
1586            RawPacket::pack_raw_with_hash(flags, 0, link_id, None, context, &encrypted)
1587                .map_err(|_| LinkDatagramError::PacketEncodingFailed)?;
1588        actions.push(LinkManagerAction::SendPacket {
1589            raw,
1590            dest_type: constants::DESTINATION_LINK,
1591            attached_interface: None,
1592        });
1593        Ok(actions)
1594    }
1595
1596    /// Send an identify message on a link (initiator reveals identity to responder).
1597    pub fn identify(
1598        &self,
1599        link_id: &LinkId,
1600        identity: &rns_crypto::identity::Identity,
1601        rng: &mut dyn Rng,
1602    ) -> Vec<LinkManagerAction> {
1603        let link = match self.links.get(link_id) {
1604            Some(l) => l,
1605            None => return Vec::new(),
1606        };
1607
1608        let encrypted = match link.engine.build_identify(identity, rng) {
1609            Ok(e) => e,
1610            Err(_) => return Vec::new(),
1611        };
1612
1613        let flags = PacketFlags {
1614            header_type: constants::HEADER_1,
1615            context_flag: constants::FLAG_UNSET,
1616            transport_type: constants::TRANSPORT_BROADCAST,
1617            destination_type: constants::DESTINATION_LINK,
1618            packet_type: constants::PACKET_TYPE_DATA,
1619        };
1620
1621        let mut actions = Vec::new();
1622        if let Ok((raw, _packet_hash)) = RawPacket::pack_raw_with_hash(
1623            flags,
1624            0,
1625            link_id,
1626            None,
1627            constants::CONTEXT_LINKIDENTIFY,
1628            &encrypted,
1629        ) {
1630            actions.push(LinkManagerAction::SendPacket {
1631                raw,
1632                dest_type: constants::DESTINATION_LINK,
1633                attached_interface: None,
1634            });
1635        }
1636        actions
1637    }
1638
1639    /// Tear down a link.
1640    pub fn teardown_link(&mut self, link_id: &LinkId) -> Vec<LinkManagerAction> {
1641        let link = match self.links.get_mut(link_id) {
1642            Some(l) => l,
1643            None => return Vec::new(),
1644        };
1645
1646        let previous_state = link.engine.state();
1647        if previous_state == LinkState::Closed {
1648            return Vec::new();
1649        }
1650
1651        let encrypted_close = if previous_state == LinkState::Pending {
1652            None
1653        } else {
1654            let mut rng = OsRng;
1655            link.engine.encrypt(link_id, &mut rng).ok()
1656        };
1657        let teardown_actions = link.engine.teardown();
1658        if let Some(ref mut channel) = link.channel {
1659            channel.shutdown();
1660        }
1661
1662        let mut actions = self.process_link_actions(link_id, &teardown_actions);
1663
1664        // Pending links have not established link encryption and are closed
1665        // locally without transmitting LINKCLOSE.
1666        if previous_state == LinkState::Pending {
1667            return actions;
1668        }
1669
1670        // Send LINKCLOSE packet
1671        let flags = PacketFlags {
1672            header_type: constants::HEADER_1,
1673            context_flag: constants::FLAG_UNSET,
1674            transport_type: constants::TRANSPORT_BROADCAST,
1675            destination_type: constants::DESTINATION_LINK,
1676            packet_type: constants::PACKET_TYPE_DATA,
1677        };
1678        if let Some(encrypted_close) = encrypted_close {
1679            if let Ok((raw, _packet_hash)) = RawPacket::pack_raw_with_hash(
1680                flags,
1681                0,
1682                link_id,
1683                None,
1684                constants::CONTEXT_LINKCLOSE,
1685                &encrypted_close,
1686            ) {
1687                actions.push(LinkManagerAction::SendPacket {
1688                    raw,
1689                    dest_type: constants::DESTINATION_LINK,
1690                    attached_interface: None,
1691                });
1692            }
1693        }
1694
1695        actions
1696    }
1697
1698    /// Tear down all managed links.
1699    pub fn teardown_all_links(&mut self) -> Vec<LinkManagerAction> {
1700        let link_ids: Vec<LinkId> = self.links.keys().copied().collect();
1701        let mut actions = Vec::new();
1702        for link_id in link_ids {
1703            actions.extend(self.teardown_link(&link_id));
1704        }
1705        actions
1706    }
1707
1708    /// Periodic tick: check keepalive, stale, timeouts for all links.
1709    pub fn tick(&mut self, rng: &mut dyn Rng) -> Vec<LinkManagerAction> {
1710        let now = time::now();
1711        let mut all_actions = Vec::new();
1712
1713        // Collect link_ids to avoid borrow issues
1714        let link_ids: Vec<LinkId> = self.links.keys().copied().collect();
1715
1716        for link_id in &link_ids {
1717            let link = match self.links.get_mut(link_id) {
1718                Some(l) => l,
1719                None => continue,
1720            };
1721
1722            // Tick the engine
1723            let tick_actions = link.engine.tick(now);
1724            all_actions.extend(self.process_link_actions(link_id, &tick_actions));
1725
1726            // Check if keepalive is needed
1727            let link = match self.links.get_mut(link_id) {
1728                Some(l) => l,
1729                None => continue,
1730            };
1731            if link.engine.needs_keepalive(now) {
1732                log::debug!("Link keepalive probe queued: link={:02x?}", &link_id[..4]);
1733                // Only initiators reach this branch. Send the upstream 0xff
1734                // probe; responders conditionally return 0xfe on receipt.
1735                let flags = PacketFlags {
1736                    header_type: constants::HEADER_1,
1737                    context_flag: constants::FLAG_UNSET,
1738                    transport_type: constants::TRANSPORT_BROADCAST,
1739                    destination_type: constants::DESTINATION_LINK,
1740                    packet_type: constants::PACKET_TYPE_DATA,
1741                };
1742                if let Ok((raw, _packet_hash)) = RawPacket::pack_raw_with_hash(
1743                    flags,
1744                    0,
1745                    link_id,
1746                    None,
1747                    constants::CONTEXT_KEEPALIVE,
1748                    &[0xff],
1749                ) {
1750                    all_actions.push(LinkManagerAction::SendPacket {
1751                        raw,
1752                        dest_type: constants::DESTINATION_LINK,
1753                        attached_interface: None,
1754                    });
1755                    link.engine.record_outbound(now, true);
1756                }
1757            }
1758
1759            if let Some(channel) = link.channel.as_mut() {
1760                let chan_actions = channel.tick(now);
1761                let _ = channel;
1762                let _ = link;
1763                all_actions.extend(self.process_channel_actions(link_id, chan_actions, rng));
1764            }
1765        }
1766
1767        // Tick resource senders and receivers
1768        for link_id in &link_ids {
1769            let link = match self.links.get_mut(link_id) {
1770                Some(l) => l,
1771                None => continue,
1772            };
1773
1774            // Tick outgoing resources (senders)
1775            let mut sender_actions = Vec::new();
1776            for sender in &mut link.outgoing_resources {
1777                sender_actions.extend(sender.tick(now));
1778            }
1779
1780            // Tick incoming resources (receivers)
1781            let mut receiver_actions = Vec::new();
1782            for receiver in &mut link.incoming_resources {
1783                let decrypt_fn = |ciphertext: &[u8]| -> Result<Vec<u8>, ()> {
1784                    link.engine.decrypt(ciphertext).map_err(|_| ())
1785                };
1786                receiver_actions.extend(receiver.tick(now, &decrypt_fn, &Bzip2Compressor));
1787            }
1788
1789            let failed_request_ids: Vec<[u8; 16]> = link
1790                .outgoing_resources
1791                .iter()
1792                .filter(|sender| {
1793                    sender.flags.is_request
1794                        && sender.status >= rns_core::resource::ResourceStatus::Failed
1795                })
1796                .filter_map(|sender| Self::response_request_id(&sender.request_id))
1797                .collect();
1798            for request_id in failed_request_ids {
1799                link.pending_requests.remove(&request_id);
1800            }
1801            link.pending_requests
1802                .retain(|_, request| request.deadline.is_none_or(|expires_at| now <= expires_at));
1803
1804            // Clean up completed/failed resources
1805            link.outgoing_resources
1806                .retain(|s| s.status < rns_core::resource::ResourceStatus::Complete);
1807            link.incoming_resources
1808                .retain(|r| r.status < rns_core::resource::ResourceStatus::Complete);
1809            let active_split_hashes: Vec<[u8; 32]> = link
1810                .outgoing_resources
1811                .iter()
1812                .filter(|s| s.flags.split)
1813                .map(|s| s.original_hash)
1814                .collect();
1815            link.outgoing_splits.retain(|original_hash, split| {
1816                split.completed_segments < split.total_segments
1817                    && active_split_hashes.contains(original_hash)
1818            });
1819
1820            let _ = link;
1821            all_actions.extend(self.process_resource_actions(link_id, sender_actions, rng));
1822            all_actions.extend(self.process_resource_actions(link_id, receiver_actions, rng));
1823        }
1824
1825        // Clean up closed links
1826        let closed: Vec<LinkId> = self
1827            .links
1828            .iter()
1829            .filter(|(_, l)| l.engine.state() == LinkState::Closed)
1830            .map(|(id, _)| *id)
1831            .collect();
1832        for id in closed {
1833            self.links.remove(&id);
1834            all_actions.push(LinkManagerAction::DeregisterLinkDest { link_id: id });
1835        }
1836
1837        all_actions
1838    }
1839
1840    /// Check if a destination hash is a known link_id managed by this manager.
1841    pub fn is_link_destination(&self, dest_hash: &[u8; 16]) -> bool {
1842        self.links.contains_key(dest_hash) || self.link_destinations.contains_key(dest_hash)
1843    }
1844
1845    /// Get the state of a link.
1846    pub fn link_state(&self, link_id: &LinkId) -> Option<LinkState> {
1847        self.links.get(link_id).map(|l| l.engine.state())
1848    }
1849
1850    /// Get the RTT of a link.
1851    pub fn link_rtt(&self, link_id: &LinkId) -> Option<f64> {
1852        self.links.get(link_id).and_then(|l| l.engine.rtt())
1853    }
1854
1855    /// Update the RTT of a link (e.g., after path redirect to a direct connection).
1856    pub fn set_link_rtt(&mut self, link_id: &LinkId, rtt: f64) {
1857        if let Some(link) = self.links.get_mut(link_id) {
1858            link.engine.set_rtt(rtt);
1859        }
1860    }
1861
1862    /// Reset the inbound timer for a link (e.g., after path redirect).
1863    pub fn record_link_inbound(&mut self, link_id: &LinkId) {
1864        if let Some(link) = self.links.get_mut(link_id) {
1865            link.engine.record_inbound(time::now());
1866        }
1867    }
1868
1869    /// Update the MTU of a link (e.g., after path redirect to a different interface).
1870    pub fn set_link_mtu(&mut self, link_id: &LinkId, mtu: u32) {
1871        if let Some(link) = self.links.get_mut(link_id) {
1872            link.engine.set_mtu(mtu);
1873        }
1874    }
1875
1876    /// Get the number of tracked links in any state.
1877    pub fn link_count(&self) -> usize {
1878        self.links.len()
1879    }
1880
1881    /// Get the number of established, active links.
1882    pub fn active_link_count(&self) -> usize {
1883        self.links
1884            .values()
1885            .filter(|link| link.engine.state() == LinkState::Active)
1886            .count()
1887    }
1888
1889    /// Get the number of active resource transfers across all links.
1890    pub fn resource_transfer_count(&self) -> usize {
1891        self.links
1892            .values()
1893            .map(|managed| {
1894                managed
1895                    .incoming_resources
1896                    .iter()
1897                    .filter(|resource| !resource.flags.split && resource.assembly_id().is_none())
1898                    .count()
1899                    + managed.incoming_splits.len()
1900                    + managed
1901                        .outgoing_resources
1902                        .iter()
1903                        .filter(|resource| !resource.flags.split)
1904                        .count()
1905                    + managed.outgoing_splits.len()
1906            })
1907            .sum::<usize>()
1908            + self
1909                .receive_worker
1910                .as_ref()
1911                .map_or(0, |worker| worker.len())
1912    }
1913
1914    /// Cancel all active resource transfers and return the generated actions.
1915    pub fn cancel_all_resources(&mut self, rng: &mut dyn Rng) -> Vec<LinkManagerAction> {
1916        let link_ids: Vec<LinkId> = self.links.keys().copied().collect();
1917        let mut all_actions = Vec::new();
1918
1919        for link_id in &link_ids {
1920            let link = match self.links.get_mut(link_id) {
1921                Some(l) => l,
1922                None => continue,
1923            };
1924
1925            let mut sender_actions = Vec::new();
1926            for sender in &mut link.outgoing_resources {
1927                sender_actions.extend(sender.cancel());
1928            }
1929
1930            let mut receiver_actions = Vec::new();
1931            for receiver in &mut link.incoming_resources {
1932                receiver_actions.extend(receiver.cancel());
1933            }
1934
1935            link.outgoing_resources
1936                .retain(|s| s.status < rns_core::resource::ResourceStatus::Complete);
1937            link.incoming_resources
1938                .retain(|r| r.status < rns_core::resource::ResourceStatus::Complete);
1939            link.outgoing_splits.clear();
1940            all_actions.extend(link.outgoing_streams.drain().map(|(_, stream)| {
1941                LinkManagerAction::ResourceStreamFailed {
1942                    link_id: *link_id,
1943                    transfer_id: stream.transfer_id,
1944                    error: ResourceTransferError::Cancelled,
1945                }
1946            }));
1947            link.incoming_splits.clear();
1948
1949            let _ = link;
1950            all_actions.extend(self.process_resource_actions(link_id, sender_actions, rng));
1951            all_actions.extend(self.process_resource_actions(link_id, receiver_actions, rng));
1952        }
1953
1954        all_actions
1955    }
1956
1957    /// Get information about all active links.
1958    pub fn link_entries(&self) -> Vec<crate::event::LinkInfoEntry> {
1959        self.links
1960            .iter()
1961            .map(|(link_id, managed)| {
1962                let state = match managed.engine.state() {
1963                    LinkState::Pending => "pending",
1964                    LinkState::Handshake => "handshake",
1965                    LinkState::Active => "active",
1966                    LinkState::Stale => "stale",
1967                    LinkState::Closed => "closed",
1968                };
1969                crate::event::LinkInfoEntry {
1970                    link_id: *link_id,
1971                    pending_send_packets: 0,
1972                    waiting_send_packets: 0,
1973                    state: state.to_string(),
1974                    is_initiator: managed.engine.is_initiator(),
1975                    dest_hash: managed.dest_hash,
1976                    remote_identity: managed.remote_identity.as_ref().map(|(h, _)| *h),
1977                    rtt: managed.engine.rtt(),
1978                    expected_hops: managed.engine.expected_hops(),
1979                    mdu: managed.engine.mdu(),
1980                    tx_packets: managed.engine.tx_packets(),
1981                    rx_packets: managed.engine.rx_packets(),
1982                    tx_bytes: managed.engine.tx_bytes(),
1983                    rx_bytes: managed.engine.rx_bytes(),
1984                    channel_window: managed.channel.as_ref().map(|c| c.window()),
1985                    channel_outstanding: managed.channel.as_ref().map(|c| c.outstanding()),
1986                    pending_channel_packets: managed.pending_channel_packets.len(),
1987                    channel_send_ok: managed.channel_send_ok,
1988                    channel_send_not_ready: managed.channel_send_not_ready,
1989                    channel_send_too_big: managed.channel_send_too_big,
1990                    channel_send_other_error: managed.channel_send_other_error,
1991                    channel_messages_received: managed.channel_messages_received,
1992                    channel_proofs_sent: managed.channel_proofs_sent,
1993                    channel_proofs_received: managed.channel_proofs_received,
1994                }
1995            })
1996            .collect()
1997    }
1998
1999    /// Account a packed outbound action at the single driver dispatch point.
2000    /// LINKREQUEST is accounted when its engine is created since its wire
2001    /// destination is not the resulting link ID.
2002    pub fn record_outbound_packet(&mut self, packet: &RawPacket) {
2003        if packet.flags.destination_type == constants::DESTINATION_LINK {
2004            if let Some(link) = self.links.get_mut(&packet.destination_hash) {
2005                link.engine.record_outbound_traffic(packet.data().len());
2006            }
2007        }
2008    }
2009
2010    /// Get information about all active resource transfers.
2011    pub fn resource_entries(&self) -> Vec<crate::event::ResourceInfoEntry> {
2012        let mut entries = Vec::new();
2013        for (link_id, managed) in &self.links {
2014            let resource_sdu = Self::resource_sdu_for_link(managed);
2015            for split in managed.incoming_splits.values() {
2016                let (received, total) = Self::incoming_split_progress(split, resource_sdu);
2017                entries.push(crate::event::ResourceInfoEntry {
2018                    link_id: *link_id,
2019                    direction: "incoming".to_string(),
2020                    total_parts: total,
2021                    transferred_parts: received,
2022                    complete: received >= total && total > 0,
2023                });
2024            }
2025            for recv in &managed.incoming_resources {
2026                if recv.flags.split {
2027                    continue;
2028                }
2029                let (received, total) = recv.progress();
2030                entries.push(crate::event::ResourceInfoEntry {
2031                    link_id: *link_id,
2032                    direction: "incoming".to_string(),
2033                    total_parts: total,
2034                    transferred_parts: received,
2035                    complete: received >= total && total > 0,
2036                });
2037            }
2038            for split in managed.outgoing_splits.values() {
2039                let (sent, total) = Self::outgoing_split_progress(split, resource_sdu);
2040                entries.push(crate::event::ResourceInfoEntry {
2041                    link_id: *link_id,
2042                    direction: "outgoing".to_string(),
2043                    total_parts: total,
2044                    transferred_parts: sent,
2045                    complete: sent >= total && total > 0,
2046                });
2047            }
2048            for send in &managed.outgoing_resources {
2049                if send.flags.split {
2050                    continue;
2051                }
2052                let total = send.total_parts();
2053                let sent = send.sent_parts;
2054                entries.push(crate::event::ResourceInfoEntry {
2055                    link_id: *link_id,
2056                    direction: "outgoing".to_string(),
2057                    total_parts: total,
2058                    transferred_parts: sent,
2059                    complete: sent >= total && total > 0,
2060                });
2061            }
2062        }
2063        entries
2064    }
2065
2066    /// Convert LinkActions to LinkManagerActions.
2067    fn process_link_actions(
2068        &self,
2069        link_id: &LinkId,
2070        actions: &[LinkAction],
2071    ) -> Vec<LinkManagerAction> {
2072        let mut result = Vec::new();
2073        for action in actions {
2074            match action {
2075                LinkAction::StateChanged {
2076                    new_state, reason, ..
2077                } => {
2078                    log::debug!(
2079                        "Link state changed: link={:02x?} state={new_state:?} reason={reason:?}",
2080                        &link_id[..4]
2081                    );
2082                    if new_state == &LinkState::Closed {
2083                        result.push(LinkManagerAction::LinkClosed {
2084                            link_id: *link_id,
2085                            reason: *reason,
2086                        });
2087                    }
2088                }
2089                LinkAction::LinkEstablished {
2090                    rtt, is_initiator, ..
2091                } => {
2092                    let keepalive = self
2093                        .links
2094                        .get(link_id)
2095                        .map(|link| link.engine.keepalive_interval())
2096                        .unwrap_or_default();
2097                    log::debug!(
2098                        "Link timers established: link={:02x?} rtt={rtt:.6} keepalive={keepalive:.6} stale={:.6} initiator={is_initiator}",
2099                        &link_id[..4],
2100                        keepalive * rns_core::constants::LINK_STALE_FACTOR
2101                    );
2102                    let dest_hash = self
2103                        .links
2104                        .get(link_id)
2105                        .map(|l| l.dest_hash)
2106                        .unwrap_or([0u8; 16]);
2107                    let hops = self
2108                        .links
2109                        .get(link_id)
2110                        .map(|link| link.engine.expected_hops())
2111                        .unwrap_or(rns_core::constants::PATHFINDER_M);
2112                    let rebalanced_at = self
2113                        .links
2114                        .get(link_id)
2115                        .and_then(|link| link.engine.rebalanced_at());
2116                    result.push(LinkManagerAction::LinkEstablished {
2117                        link_id: *link_id,
2118                        dest_hash,
2119                        hops,
2120                        rebalanced_at,
2121                        rtt: *rtt,
2122                        is_initiator: *is_initiator,
2123                    });
2124                }
2125                LinkAction::RemoteIdentified {
2126                    identity_hash,
2127                    public_key,
2128                    ..
2129                } => {
2130                    result.push(LinkManagerAction::RemoteIdentified {
2131                        link_id: *link_id,
2132                        identity_hash: *identity_hash,
2133                        public_key: *public_key,
2134                    });
2135                }
2136                LinkAction::DataReceived { .. } => {
2137                    // Data delivery is handled at a higher level
2138                }
2139            }
2140        }
2141        result
2142    }
2143}
2144
2145impl Default for LinkManager {
2146    fn default() -> Self {
2147        Self::new()
2148    }
2149}
2150
2151/// Compute a path hash from a path string.
2152/// Uses truncated SHA-256 (first 16 bytes).
2153fn compute_path_hash(path: &str) -> [u8; 16] {
2154    let full = rns_core::hash::full_hash(path.as_bytes());
2155    let mut result = [0u8; 16];
2156    result.copy_from_slice(&full[..16]);
2157    result
2158}
2159
2160#[cfg(test)]
2161mod tests;