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