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;
10
11use super::compressor::Bzip2Compressor;
12use rns_core::channel::Channel;
13use rns_core::constants;
14use rns_core::link::types::{LinkId, LinkState, TeardownReason};
15use rns_core::link::{LinkAction, LinkEngine, LinkMode};
16use rns_core::packet::{PacketFlags, RawPacket};
17use rns_core::resource::{ResourceAction, ResourceReceiver, ResourceSender};
18use rns_crypto::ed25519::Ed25519PrivateKey;
19use rns_crypto::Rng;
20
21use super::time;
22
23/// Resource acceptance strategy.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ResourceStrategy {
26    /// Reject all incoming resources.
27    AcceptNone,
28    /// Accept all incoming resources automatically.
29    AcceptAll,
30    /// Query the application callback for each resource.
31    AcceptApp,
32}
33
34impl Default for ResourceStrategy {
35    fn default() -> Self {
36        ResourceStrategy::AcceptNone
37    }
38}
39
40/// A managed link wrapping LinkEngine + optional Channel + resources.
41struct ManagedLink {
42    engine: LinkEngine,
43    channel: Option<Channel>,
44    /// Destination hash this link belongs to.
45    dest_hash: [u8; 16],
46    /// Remote identity (hash, public_key) once identified.
47    remote_identity: Option<([u8; 16], [u8; 64])>,
48    /// Destination's Ed25519 signing public key (for initiator to verify LRPROOF).
49    dest_sig_pub_bytes: Option<[u8; 32]>,
50    /// Active incoming resource transfers.
51    incoming_resources: Vec<ResourceReceiver>,
52    /// Active outgoing resource transfers.
53    outgoing_resources: Vec<ResourceSender>,
54    /// Resource acceptance strategy.
55    resource_strategy: ResourceStrategy,
56}
57
58/// A registered link destination that can accept incoming LINKREQUEST.
59struct LinkDestination {
60    sig_prv: Ed25519PrivateKey,
61    sig_pub_bytes: [u8; 32],
62    resource_strategy: ResourceStrategy,
63}
64
65/// A registered request handler for a path.
66struct RequestHandlerEntry {
67    /// The path this handler serves (e.g. "/status").
68    path: String,
69    /// The truncated hash of the path (first 16 bytes of SHA-256).
70    path_hash: [u8; 16],
71    /// Access control: None means allow all, Some(list) means allow only listed identities.
72    allowed_list: Option<Vec<[u8; 16]>>,
73    /// Handler function: (link_id, path, request_id, data, remote_identity) -> Option<response_data>.
74    handler:
75        Box<dyn Fn(LinkId, &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<Vec<u8>> + Send>,
76}
77
78/// Actions produced by LinkManager for the driver to dispatch.
79#[derive(Debug)]
80pub enum LinkManagerAction {
81    /// Send a packet via the transport engine outbound path.
82    SendPacket {
83        raw: Vec<u8>,
84        dest_type: u8,
85        attached_interface: Option<rns_core::transport::types::InterfaceId>,
86    },
87    /// Link established — notify callbacks.
88    LinkEstablished {
89        link_id: LinkId,
90        dest_hash: [u8; 16],
91        rtt: f64,
92        is_initiator: bool,
93    },
94    /// Link closed — notify callbacks.
95    LinkClosed {
96        link_id: LinkId,
97        reason: Option<TeardownReason>,
98    },
99    /// Remote peer identified — notify callbacks.
100    RemoteIdentified {
101        link_id: LinkId,
102        identity_hash: [u8; 16],
103        public_key: [u8; 64],
104    },
105    /// Register a link_id as local destination in transport (for receiving link data).
106    RegisterLinkDest { link_id: LinkId },
107    /// Deregister a link_id from transport local destinations.
108    DeregisterLinkDest { link_id: LinkId },
109    /// A management request that needs to be handled by the driver.
110    /// The driver has access to engine state needed to build the response.
111    ManagementRequest {
112        link_id: LinkId,
113        path_hash: [u8; 16],
114        /// The request data (msgpack-encoded Value from the request array).
115        data: Vec<u8>,
116        /// The request_id (truncated hash of the packed request).
117        request_id: [u8; 16],
118        remote_identity: Option<([u8; 16], [u8; 64])>,
119    },
120    /// Resource data fully received and assembled.
121    ResourceReceived {
122        link_id: LinkId,
123        data: Vec<u8>,
124        metadata: Option<Vec<u8>>,
125    },
126    /// Resource transfer completed (proof validated on sender side).
127    ResourceCompleted { link_id: LinkId },
128    /// Resource transfer failed.
129    ResourceFailed { link_id: LinkId, error: String },
130    /// Resource transfer progress update.
131    ResourceProgress {
132        link_id: LinkId,
133        received: usize,
134        total: usize,
135    },
136    /// Query application whether to accept an incoming resource (for AcceptApp strategy).
137    ResourceAcceptQuery {
138        link_id: LinkId,
139        resource_hash: Vec<u8>,
140        transfer_size: u64,
141        has_metadata: bool,
142    },
143    /// Channel message received on a link.
144    ChannelMessageReceived {
145        link_id: LinkId,
146        msgtype: u16,
147        payload: Vec<u8>,
148    },
149    /// Generic link data received (CONTEXT_NONE).
150    LinkDataReceived {
151        link_id: LinkId,
152        context: u8,
153        data: Vec<u8>,
154    },
155    /// Response received on a link.
156    ResponseReceived {
157        link_id: LinkId,
158        request_id: [u8; 16],
159        data: Vec<u8>,
160    },
161    /// A link request was received (for hook notification).
162    LinkRequestReceived {
163        link_id: LinkId,
164        receiving_interface: rns_core::transport::types::InterfaceId,
165    },
166}
167
168/// Manages multiple links, link destinations, and request/response.
169pub struct LinkManager {
170    links: HashMap<LinkId, ManagedLink>,
171    link_destinations: HashMap<[u8; 16], LinkDestination>,
172    request_handlers: Vec<RequestHandlerEntry>,
173    /// Path hashes that should be handled externally (by the driver) rather than
174    /// by registered handler closures. Used for management destinations.
175    management_paths: Vec<[u8; 16]>,
176}
177
178impl LinkManager {
179    /// Create a new empty link manager.
180    pub fn new() -> Self {
181        LinkManager {
182            links: HashMap::new(),
183            link_destinations: HashMap::new(),
184            request_handlers: Vec::new(),
185            management_paths: Vec::new(),
186        }
187    }
188
189    /// Register a path hash as a management path.
190    /// Management requests are returned as ManagementRequest actions
191    /// for the driver to handle (since they need access to engine state).
192    pub fn register_management_path(&mut self, path_hash: [u8; 16]) {
193        if !self.management_paths.contains(&path_hash) {
194            self.management_paths.push(path_hash);
195        }
196    }
197
198    /// Get the derived session key for a link (needed for hole-punch token derivation).
199    pub fn get_derived_key(&self, link_id: &LinkId) -> Option<Vec<u8>> {
200        self.links
201            .get(link_id)
202            .and_then(|link| link.engine.derived_key().map(|dk| dk.to_vec()))
203    }
204
205    /// Register a destination that can accept incoming links.
206    pub fn register_link_destination(
207        &mut self,
208        dest_hash: [u8; 16],
209        sig_prv: Ed25519PrivateKey,
210        sig_pub_bytes: [u8; 32],
211        resource_strategy: ResourceStrategy,
212    ) {
213        self.link_destinations.insert(
214            dest_hash,
215            LinkDestination {
216                sig_prv,
217                sig_pub_bytes,
218                resource_strategy,
219            },
220        );
221    }
222
223    /// Deregister a link destination.
224    pub fn deregister_link_destination(&mut self, dest_hash: &[u8; 16]) {
225        self.link_destinations.remove(dest_hash);
226    }
227
228    /// Register a request handler for a given path.
229    ///
230    /// `path`: the request path string (e.g. "/status")
231    /// `allowed_list`: None = allow all, Some(list) = restrict to these identity hashes
232    /// `handler`: called with (link_id, path, request_data, remote_identity) -> Option<response>
233    pub fn register_request_handler<F>(
234        &mut self,
235        path: &str,
236        allowed_list: Option<Vec<[u8; 16]>>,
237        handler: F,
238    ) where
239        F: Fn(LinkId, &str, &[u8], Option<&([u8; 16], [u8; 64])>) -> Option<Vec<u8>>
240            + Send
241            + 'static,
242    {
243        let path_hash = compute_path_hash(path);
244        self.request_handlers.push(RequestHandlerEntry {
245            path: path.to_string(),
246            path_hash,
247            allowed_list,
248            handler: Box::new(handler),
249        });
250    }
251
252    /// Create an outbound link to a destination.
253    ///
254    /// `dest_sig_pub_bytes` is the destination's Ed25519 signing public key
255    /// (needed to verify LRPROOF). In Python this comes from the Destination's Identity.
256    ///
257    /// Returns `(link_id, actions)`. The first action will be a SendPacket with
258    /// the LINKREQUEST.
259    pub fn create_link(
260        &mut self,
261        dest_hash: &[u8; 16],
262        dest_sig_pub_bytes: &[u8; 32],
263        hops: u8,
264        mtu: u32,
265        rng: &mut dyn Rng,
266    ) -> (LinkId, Vec<LinkManagerAction>) {
267        let mode = LinkMode::Aes256Cbc;
268        let (mut engine, request_data) =
269            LinkEngine::new_initiator(dest_hash, hops, mode, Some(mtu), time::now(), rng);
270
271        // Build the LINKREQUEST packet to compute link_id
272        let flags = PacketFlags {
273            header_type: constants::HEADER_1,
274            context_flag: constants::FLAG_UNSET,
275            transport_type: constants::TRANSPORT_BROADCAST,
276            destination_type: constants::DESTINATION_SINGLE,
277            packet_type: constants::PACKET_TYPE_LINKREQUEST,
278        };
279
280        let packet = match RawPacket::pack(
281            flags,
282            0,
283            dest_hash,
284            None,
285            constants::CONTEXT_NONE,
286            &request_data,
287        ) {
288            Ok(p) => p,
289            Err(_) => {
290                // Should not happen with valid data
291                return ([0u8; 16], Vec::new());
292            }
293        };
294
295        engine.set_link_id_from_hashable(&packet.get_hashable_part(), request_data.len());
296        let link_id = *engine.link_id();
297
298        let managed = ManagedLink {
299            engine,
300            channel: None,
301            dest_hash: *dest_hash,
302            remote_identity: None,
303            dest_sig_pub_bytes: Some(*dest_sig_pub_bytes),
304            incoming_resources: Vec::new(),
305            outgoing_resources: Vec::new(),
306            resource_strategy: ResourceStrategy::default(),
307        };
308        self.links.insert(link_id, managed);
309
310        let mut actions = Vec::new();
311        // Register the link_id as a local destination so we can receive LRPROOF
312        actions.push(LinkManagerAction::RegisterLinkDest { link_id });
313        // Send the LINKREQUEST packet
314        actions.push(LinkManagerAction::SendPacket {
315            raw: packet.raw,
316            dest_type: constants::DESTINATION_LINK,
317            attached_interface: None,
318        });
319
320        (link_id, actions)
321    }
322
323    /// Handle a packet delivered locally (via DeliverLocal).
324    ///
325    /// Returns actions for the driver to dispatch. The `dest_hash` is the
326    /// packet's destination_hash field. `raw` is the full packet bytes.
327    /// `packet_hash` is the SHA-256 hash.
328    pub fn handle_local_delivery(
329        &mut self,
330        dest_hash: [u8; 16],
331        raw: &[u8],
332        packet_hash: [u8; 32],
333        receiving_interface: rns_core::transport::types::InterfaceId,
334        rng: &mut dyn Rng,
335    ) -> Vec<LinkManagerAction> {
336        let packet = match RawPacket::unpack(raw) {
337            Ok(p) => p,
338            Err(_) => return Vec::new(),
339        };
340
341        match packet.flags.packet_type {
342            constants::PACKET_TYPE_LINKREQUEST => {
343                self.handle_linkrequest(&dest_hash, &packet, receiving_interface, rng)
344            }
345            constants::PACKET_TYPE_PROOF if packet.context == constants::CONTEXT_LRPROOF => {
346                // LRPROOF: dest_hash is the link_id
347                self.handle_lrproof(&dest_hash, &packet, rng)
348            }
349            constants::PACKET_TYPE_DATA => {
350                self.handle_link_data(&dest_hash, &packet, packet_hash, rng)
351            }
352            _ => Vec::new(),
353        }
354    }
355
356    /// Handle an incoming LINKREQUEST packet.
357    fn handle_linkrequest(
358        &mut self,
359        dest_hash: &[u8; 16],
360        packet: &RawPacket,
361        receiving_interface: rns_core::transport::types::InterfaceId,
362        rng: &mut dyn Rng,
363    ) -> Vec<LinkManagerAction> {
364        // Look up the link destination
365        let ld = match self.link_destinations.get(dest_hash) {
366            Some(ld) => ld,
367            None => return Vec::new(),
368        };
369
370        let hashable = packet.get_hashable_part();
371        let now = time::now();
372
373        // Create responder engine
374        let (engine, lrproof_data) = match LinkEngine::new_responder(
375            &ld.sig_prv,
376            &ld.sig_pub_bytes,
377            &packet.data,
378            &hashable,
379            dest_hash,
380            packet.hops,
381            now,
382            rng,
383        ) {
384            Ok(r) => r,
385            Err(e) => {
386                log::debug!("LINKREQUEST rejected: {}", e);
387                return Vec::new();
388            }
389        };
390
391        let link_id = *engine.link_id();
392
393        let managed = ManagedLink {
394            engine,
395            channel: None,
396            dest_hash: *dest_hash,
397            remote_identity: None,
398            dest_sig_pub_bytes: None,
399            incoming_resources: Vec::new(),
400            outgoing_resources: Vec::new(),
401            resource_strategy: ld.resource_strategy,
402        };
403        self.links.insert(link_id, managed);
404
405        // Build LRPROOF packet: type=PROOF, context=LRPROOF, dest=link_id
406        let flags = PacketFlags {
407            header_type: constants::HEADER_1,
408            context_flag: constants::FLAG_UNSET,
409            transport_type: constants::TRANSPORT_BROADCAST,
410            destination_type: constants::DESTINATION_LINK,
411            packet_type: constants::PACKET_TYPE_PROOF,
412        };
413
414        let mut actions = Vec::new();
415
416        // Register link_id as local destination so we receive link data
417        actions.push(LinkManagerAction::RegisterLinkDest { link_id });
418
419        if let Ok(pkt) = RawPacket::pack(
420            flags,
421            0,
422            &link_id,
423            None,
424            constants::CONTEXT_LRPROOF,
425            &lrproof_data,
426        ) {
427            actions.push(LinkManagerAction::SendPacket {
428                raw: pkt.raw,
429                dest_type: constants::DESTINATION_LINK,
430                attached_interface: None,
431            });
432        }
433
434        // Notify hook system about the incoming link request
435        actions.push(LinkManagerAction::LinkRequestReceived {
436            link_id,
437            receiving_interface,
438        });
439
440        actions
441    }
442
443    /// Handle an incoming LRPROOF packet (initiator side).
444    fn handle_lrproof(
445        &mut self,
446        link_id_bytes: &[u8; 16],
447        packet: &RawPacket,
448        rng: &mut dyn Rng,
449    ) -> Vec<LinkManagerAction> {
450        let link = match self.links.get_mut(link_id_bytes) {
451            Some(l) => l,
452            None => return Vec::new(),
453        };
454
455        if link.engine.state() != LinkState::Pending || !link.engine.is_initiator() {
456            return Vec::new();
457        }
458
459        // The destination's signing pub key was stored when create_link was called
460        let dest_sig_pub_bytes = match link.dest_sig_pub_bytes {
461            Some(b) => b,
462            None => {
463                log::debug!("LRPROOF: no destination signing key available");
464                return Vec::new();
465            }
466        };
467
468        let now = time::now();
469        let (lrrtt_encrypted, link_actions) =
470            match link
471                .engine
472                .handle_lrproof(&packet.data, &dest_sig_pub_bytes, now, rng)
473            {
474                Ok(r) => r,
475                Err(e) => {
476                    log::debug!("LRPROOF validation failed: {}", e);
477                    return Vec::new();
478                }
479            };
480
481        let link_id = *link.engine.link_id();
482        let mut actions = Vec::new();
483
484        // Process link actions (StateChanged, LinkEstablished)
485        actions.extend(self.process_link_actions(&link_id, &link_actions));
486
487        // Send LRRTT: type=DATA, context=LRRTT, dest=link_id
488        let flags = PacketFlags {
489            header_type: constants::HEADER_1,
490            context_flag: constants::FLAG_UNSET,
491            transport_type: constants::TRANSPORT_BROADCAST,
492            destination_type: constants::DESTINATION_LINK,
493            packet_type: constants::PACKET_TYPE_DATA,
494        };
495
496        if let Ok(pkt) = RawPacket::pack(
497            flags,
498            0,
499            &link_id,
500            None,
501            constants::CONTEXT_LRRTT,
502            &lrrtt_encrypted,
503        ) {
504            actions.push(LinkManagerAction::SendPacket {
505                raw: pkt.raw,
506                dest_type: constants::DESTINATION_LINK,
507                attached_interface: None,
508            });
509        }
510
511        // Initialize channel now that link is active
512        if let Some(link) = self.links.get_mut(&link_id) {
513            if link.engine.state() == LinkState::Active {
514                let rtt = link.engine.rtt().unwrap_or(1.0);
515                link.channel = Some(Channel::new(rtt));
516            }
517        }
518
519        actions
520    }
521
522    /// Handle DATA packets on an established link.
523    ///
524    /// Structured to avoid borrow checker issues: we perform engine operations
525    /// on the link, collect intermediate results, drop the mutable borrow, then
526    /// call self methods that need immutable access.
527    fn handle_link_data(
528        &mut self,
529        link_id_bytes: &[u8; 16],
530        packet: &RawPacket,
531        packet_hash: [u8; 32],
532        rng: &mut dyn Rng,
533    ) -> Vec<LinkManagerAction> {
534        // First pass: perform engine operations, collect results
535        enum LinkDataResult {
536            Lrrtt {
537                link_id: LinkId,
538                link_actions: Vec<LinkAction>,
539            },
540            Identify {
541                link_id: LinkId,
542                link_actions: Vec<LinkAction>,
543            },
544            Keepalive {
545                link_id: LinkId,
546                inbound_actions: Vec<LinkAction>,
547            },
548            LinkClose {
549                link_id: LinkId,
550                teardown_actions: Vec<LinkAction>,
551            },
552            Channel {
553                link_id: LinkId,
554                inbound_actions: Vec<LinkAction>,
555                plaintext: Vec<u8>,
556            },
557            Request {
558                link_id: LinkId,
559                inbound_actions: Vec<LinkAction>,
560                plaintext: Vec<u8>,
561            },
562            Response {
563                link_id: LinkId,
564                inbound_actions: Vec<LinkAction>,
565                plaintext: Vec<u8>,
566            },
567            Generic {
568                link_id: LinkId,
569                inbound_actions: Vec<LinkAction>,
570                plaintext: Vec<u8>,
571                context: u8,
572                packet_hash: [u8; 32],
573            },
574            /// Resource advertisement (link-decrypted).
575            ResourceAdv {
576                link_id: LinkId,
577                inbound_actions: Vec<LinkAction>,
578                plaintext: Vec<u8>,
579            },
580            /// Resource part request (link-decrypted).
581            ResourceReq {
582                link_id: LinkId,
583                inbound_actions: Vec<LinkAction>,
584                plaintext: Vec<u8>,
585            },
586            /// Resource hashmap update (link-decrypted).
587            ResourceHmu {
588                link_id: LinkId,
589                inbound_actions: Vec<LinkAction>,
590                plaintext: Vec<u8>,
591            },
592            /// Resource part data (NOT link-decrypted; parts are pre-encrypted by ResourceSender).
593            ResourcePart {
594                link_id: LinkId,
595                inbound_actions: Vec<LinkAction>,
596                raw_data: Vec<u8>,
597            },
598            /// Resource proof (feed to sender).
599            ResourcePrf {
600                link_id: LinkId,
601                inbound_actions: Vec<LinkAction>,
602                plaintext: Vec<u8>,
603            },
604            /// Resource cancel from initiator (link-decrypted).
605            ResourceIcl {
606                link_id: LinkId,
607                inbound_actions: Vec<LinkAction>,
608            },
609            /// Resource cancel from receiver (link-decrypted).
610            ResourceRcl {
611                link_id: LinkId,
612                inbound_actions: Vec<LinkAction>,
613            },
614            Error,
615        }
616
617        let result = {
618            let link = match self.links.get_mut(link_id_bytes) {
619                Some(l) => l,
620                None => return Vec::new(),
621            };
622
623            match packet.context {
624                constants::CONTEXT_LRRTT => {
625                    match link.engine.handle_lrrtt(&packet.data, time::now()) {
626                        Ok(link_actions) => {
627                            let link_id = *link.engine.link_id();
628                            LinkDataResult::Lrrtt {
629                                link_id,
630                                link_actions,
631                            }
632                        }
633                        Err(e) => {
634                            log::debug!("LRRTT handling failed: {}", e);
635                            LinkDataResult::Error
636                        }
637                    }
638                }
639                constants::CONTEXT_LINKIDENTIFY => {
640                    match link.engine.handle_identify(&packet.data) {
641                        Ok(link_actions) => {
642                            let link_id = *link.engine.link_id();
643                            link.remote_identity = link.engine.remote_identity().cloned();
644                            LinkDataResult::Identify {
645                                link_id,
646                                link_actions,
647                            }
648                        }
649                        Err(e) => {
650                            log::debug!("LINKIDENTIFY failed: {}", e);
651                            LinkDataResult::Error
652                        }
653                    }
654                }
655                constants::CONTEXT_KEEPALIVE => {
656                    let inbound_actions = link.engine.record_inbound(time::now());
657                    let link_id = *link.engine.link_id();
658                    LinkDataResult::Keepalive {
659                        link_id,
660                        inbound_actions,
661                    }
662                }
663                constants::CONTEXT_LINKCLOSE => {
664                    let teardown_actions = link.engine.handle_teardown();
665                    let link_id = *link.engine.link_id();
666                    LinkDataResult::LinkClose {
667                        link_id,
668                        teardown_actions,
669                    }
670                }
671                constants::CONTEXT_CHANNEL => match link.engine.decrypt(&packet.data) {
672                    Ok(plaintext) => {
673                        let inbound_actions = link.engine.record_inbound(time::now());
674                        let link_id = *link.engine.link_id();
675                        LinkDataResult::Channel {
676                            link_id,
677                            inbound_actions,
678                            plaintext,
679                        }
680                    }
681                    Err(_) => LinkDataResult::Error,
682                },
683                constants::CONTEXT_REQUEST => match link.engine.decrypt(&packet.data) {
684                    Ok(plaintext) => {
685                        let inbound_actions = link.engine.record_inbound(time::now());
686                        let link_id = *link.engine.link_id();
687                        LinkDataResult::Request {
688                            link_id,
689                            inbound_actions,
690                            plaintext,
691                        }
692                    }
693                    Err(_) => LinkDataResult::Error,
694                },
695                constants::CONTEXT_RESPONSE => match link.engine.decrypt(&packet.data) {
696                    Ok(plaintext) => {
697                        let inbound_actions = link.engine.record_inbound(time::now());
698                        let link_id = *link.engine.link_id();
699                        LinkDataResult::Response {
700                            link_id,
701                            inbound_actions,
702                            plaintext,
703                        }
704                    }
705                    Err(_) => LinkDataResult::Error,
706                },
707                // --- Resource contexts ---
708                constants::CONTEXT_RESOURCE_ADV => match link.engine.decrypt(&packet.data) {
709                    Ok(plaintext) => {
710                        let inbound_actions = link.engine.record_inbound(time::now());
711                        let link_id = *link.engine.link_id();
712                        LinkDataResult::ResourceAdv {
713                            link_id,
714                            inbound_actions,
715                            plaintext,
716                        }
717                    }
718                    Err(_) => LinkDataResult::Error,
719                },
720                constants::CONTEXT_RESOURCE_REQ => match link.engine.decrypt(&packet.data) {
721                    Ok(plaintext) => {
722                        let inbound_actions = link.engine.record_inbound(time::now());
723                        let link_id = *link.engine.link_id();
724                        LinkDataResult::ResourceReq {
725                            link_id,
726                            inbound_actions,
727                            plaintext,
728                        }
729                    }
730                    Err(_) => LinkDataResult::Error,
731                },
732                constants::CONTEXT_RESOURCE_HMU => match link.engine.decrypt(&packet.data) {
733                    Ok(plaintext) => {
734                        let inbound_actions = link.engine.record_inbound(time::now());
735                        let link_id = *link.engine.link_id();
736                        LinkDataResult::ResourceHmu {
737                            link_id,
738                            inbound_actions,
739                            plaintext,
740                        }
741                    }
742                    Err(_) => LinkDataResult::Error,
743                },
744                constants::CONTEXT_RESOURCE => {
745                    // Resource parts are NOT link-decrypted — they're pre-encrypted by ResourceSender
746                    let inbound_actions = link.engine.record_inbound(time::now());
747                    let link_id = *link.engine.link_id();
748                    LinkDataResult::ResourcePart {
749                        link_id,
750                        inbound_actions,
751                        raw_data: packet.data.clone(),
752                    }
753                }
754                constants::CONTEXT_RESOURCE_PRF => match link.engine.decrypt(&packet.data) {
755                    Ok(plaintext) => {
756                        let inbound_actions = link.engine.record_inbound(time::now());
757                        let link_id = *link.engine.link_id();
758                        LinkDataResult::ResourcePrf {
759                            link_id,
760                            inbound_actions,
761                            plaintext,
762                        }
763                    }
764                    Err(_) => LinkDataResult::Error,
765                },
766                constants::CONTEXT_RESOURCE_ICL => {
767                    let _ = link.engine.decrypt(&packet.data); // decrypt to validate
768                    let inbound_actions = link.engine.record_inbound(time::now());
769                    let link_id = *link.engine.link_id();
770                    LinkDataResult::ResourceIcl {
771                        link_id,
772                        inbound_actions,
773                    }
774                }
775                constants::CONTEXT_RESOURCE_RCL => {
776                    let _ = link.engine.decrypt(&packet.data); // decrypt to validate
777                    let inbound_actions = link.engine.record_inbound(time::now());
778                    let link_id = *link.engine.link_id();
779                    LinkDataResult::ResourceRcl {
780                        link_id,
781                        inbound_actions,
782                    }
783                }
784                _ => match link.engine.decrypt(&packet.data) {
785                    Ok(plaintext) => {
786                        let inbound_actions = link.engine.record_inbound(time::now());
787                        let link_id = *link.engine.link_id();
788                        LinkDataResult::Generic {
789                            link_id,
790                            inbound_actions,
791                            plaintext,
792                            context: packet.context,
793                            packet_hash,
794                        }
795                    }
796                    Err(_) => LinkDataResult::Error,
797                },
798            }
799        }; // mutable borrow of self.links dropped here
800
801        // Second pass: process results using self methods
802        let mut actions = Vec::new();
803        match result {
804            LinkDataResult::Lrrtt {
805                link_id,
806                link_actions,
807            } => {
808                actions.extend(self.process_link_actions(&link_id, &link_actions));
809                // Initialize channel
810                if let Some(link) = self.links.get_mut(&link_id) {
811                    if link.engine.state() == LinkState::Active {
812                        let rtt = link.engine.rtt().unwrap_or(1.0);
813                        link.channel = Some(Channel::new(rtt));
814                    }
815                }
816            }
817            LinkDataResult::Identify {
818                link_id,
819                link_actions,
820            } => {
821                actions.extend(self.process_link_actions(&link_id, &link_actions));
822            }
823            LinkDataResult::Keepalive {
824                link_id,
825                inbound_actions,
826            } => {
827                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
828                // record_inbound() already updated last_inbound, so the link
829                // won't go stale.  The regular tick() keepalive mechanism will
830                // send keepalives when needs_keepalive() returns true.
831                // Do NOT reply here — unconditional replies create an infinite
832                // ping-pong loop between the two link endpoints.
833            }
834            LinkDataResult::LinkClose {
835                link_id,
836                teardown_actions,
837            } => {
838                actions.extend(self.process_link_actions(&link_id, &teardown_actions));
839            }
840            LinkDataResult::Channel {
841                link_id,
842                inbound_actions,
843                plaintext,
844            } => {
845                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
846                // Feed plaintext to channel
847                if let Some(link) = self.links.get_mut(&link_id) {
848                    if let Some(ref mut channel) = link.channel {
849                        let chan_actions = channel.receive(&plaintext, time::now());
850                        // process_channel_actions needs immutable self, so collect first
851                        let _ = link;
852                        actions.extend(self.process_channel_actions(&link_id, chan_actions, rng));
853                    }
854                }
855            }
856            LinkDataResult::Request {
857                link_id,
858                inbound_actions,
859                plaintext,
860            } => {
861                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
862                actions.extend(self.handle_request(&link_id, &plaintext, rng));
863            }
864            LinkDataResult::Response {
865                link_id,
866                inbound_actions,
867                plaintext,
868            } => {
869                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
870                // Unpack msgpack response: [Bin(request_id), response_value]
871                actions.extend(self.handle_response(&link_id, &plaintext));
872            }
873            LinkDataResult::Generic {
874                link_id,
875                inbound_actions,
876                plaintext,
877                context,
878                packet_hash,
879            } => {
880                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
881                actions.push(LinkManagerAction::LinkDataReceived {
882                    link_id,
883                    context,
884                    data: plaintext,
885                });
886
887                // Generate proof for the link packet (Python: Link.prove_packet)
888                if let Some(link) = self.links.get(&link_id) {
889                    if let Some(ld) = self.link_destinations.get(&link.dest_hash) {
890                        let signature = ld.sig_prv.sign(&packet_hash);
891                        let mut proof_data = Vec::with_capacity(96);
892                        proof_data.extend_from_slice(&packet_hash);
893                        proof_data.extend_from_slice(&signature);
894
895                        let flags = PacketFlags {
896                            header_type: constants::HEADER_1,
897                            context_flag: constants::FLAG_UNSET,
898                            transport_type: constants::TRANSPORT_BROADCAST,
899                            destination_type: constants::DESTINATION_LINK,
900                            packet_type: constants::PACKET_TYPE_PROOF,
901                        };
902                        if let Ok(pkt) = RawPacket::pack(
903                            flags,
904                            0,
905                            &link_id,
906                            None,
907                            constants::CONTEXT_NONE,
908                            &proof_data,
909                        ) {
910                            actions.push(LinkManagerAction::SendPacket {
911                                raw: pkt.raw,
912                                dest_type: constants::DESTINATION_LINK,
913                                attached_interface: None,
914                            });
915                        }
916                    }
917                }
918            }
919            LinkDataResult::ResourceAdv {
920                link_id,
921                inbound_actions,
922                plaintext,
923            } => {
924                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
925                actions.extend(self.handle_resource_adv(&link_id, &plaintext, rng));
926            }
927            LinkDataResult::ResourceReq {
928                link_id,
929                inbound_actions,
930                plaintext,
931            } => {
932                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
933                actions.extend(self.handle_resource_req(&link_id, &plaintext, rng));
934            }
935            LinkDataResult::ResourceHmu {
936                link_id,
937                inbound_actions,
938                plaintext,
939            } => {
940                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
941                actions.extend(self.handle_resource_hmu(&link_id, &plaintext, rng));
942            }
943            LinkDataResult::ResourcePart {
944                link_id,
945                inbound_actions,
946                raw_data,
947            } => {
948                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
949                actions.extend(self.handle_resource_part(&link_id, &raw_data, rng));
950            }
951            LinkDataResult::ResourcePrf {
952                link_id,
953                inbound_actions,
954                plaintext,
955            } => {
956                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
957                actions.extend(self.handle_resource_prf(&link_id, &plaintext));
958            }
959            LinkDataResult::ResourceIcl {
960                link_id,
961                inbound_actions,
962            } => {
963                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
964                actions.extend(self.handle_resource_icl(&link_id));
965            }
966            LinkDataResult::ResourceRcl {
967                link_id,
968                inbound_actions,
969            } => {
970                actions.extend(self.process_link_actions(&link_id, &inbound_actions));
971                actions.extend(self.handle_resource_rcl(&link_id));
972            }
973            LinkDataResult::Error => {}
974        }
975
976        actions
977    }
978
979    /// Handle a request on a link.
980    fn handle_request(
981        &mut self,
982        link_id: &LinkId,
983        plaintext: &[u8],
984        rng: &mut dyn Rng,
985    ) -> Vec<LinkManagerAction> {
986        use rns_core::msgpack::{self, Value};
987
988        // Python-compatible format: msgpack([timestamp, Bin(path_hash), data_value])
989        let arr = match msgpack::unpack_exact(plaintext) {
990            Ok(Value::Array(arr)) if arr.len() >= 3 => arr,
991            _ => return Vec::new(),
992        };
993
994        let path_hash_bytes = match &arr[1] {
995            Value::Bin(b) if b.len() == 16 => b,
996            _ => return Vec::new(),
997        };
998        let mut path_hash = [0u8; 16];
999        path_hash.copy_from_slice(path_hash_bytes);
1000
1001        // Compute request_id = truncated_hash(packed_request_bytes)
1002        let request_id = rns_core::hash::truncated_hash(plaintext);
1003
1004        // Re-encode the data element for the handler
1005        let request_data = msgpack::pack(&arr[2]);
1006
1007        // Check if this is a management path (handled by the driver)
1008        if self.management_paths.contains(&path_hash) {
1009            let remote_identity = self
1010                .links
1011                .get(link_id)
1012                .and_then(|l| l.remote_identity)
1013                .map(|(h, k)| (h, k));
1014            return vec![LinkManagerAction::ManagementRequest {
1015                link_id: *link_id,
1016                path_hash,
1017                data: request_data,
1018                request_id,
1019                remote_identity,
1020            }];
1021        }
1022
1023        // Look up handler by path_hash
1024        let handler_idx = self
1025            .request_handlers
1026            .iter()
1027            .position(|h| h.path_hash == path_hash);
1028        let handler_idx = match handler_idx {
1029            Some(i) => i,
1030            None => return Vec::new(),
1031        };
1032
1033        // Check ACL
1034        let remote_identity = self
1035            .links
1036            .get(link_id)
1037            .and_then(|l| l.remote_identity.as_ref());
1038        let handler = &self.request_handlers[handler_idx];
1039        if let Some(ref allowed) = handler.allowed_list {
1040            match remote_identity {
1041                Some((identity_hash, _)) => {
1042                    if !allowed.contains(identity_hash) {
1043                        log::debug!("Request denied: identity not in allowed list");
1044                        return Vec::new();
1045                    }
1046                }
1047                None => {
1048                    log::debug!("Request denied: peer not identified");
1049                    return Vec::new();
1050                }
1051            }
1052        }
1053
1054        // Call handler
1055        let path = handler.path.clone();
1056        let response = (handler.handler)(*link_id, &path, &request_data, remote_identity);
1057
1058        let mut actions = Vec::new();
1059        if let Some(response_data) = response {
1060            actions.extend(self.build_response_packet(link_id, &request_id, &response_data, rng));
1061        }
1062
1063        actions
1064    }
1065
1066    /// Build a response packet for a request.
1067    /// `response_data` is the msgpack-encoded response value.
1068    fn build_response_packet(
1069        &self,
1070        link_id: &LinkId,
1071        request_id: &[u8; 16],
1072        response_data: &[u8],
1073        rng: &mut dyn Rng,
1074    ) -> Vec<LinkManagerAction> {
1075        use rns_core::msgpack::{self, Value};
1076
1077        // Python-compatible response: msgpack([Bin(request_id), response_value])
1078        let response_value = msgpack::unpack_exact(response_data)
1079            .unwrap_or_else(|_| Value::Bin(response_data.to_vec()));
1080
1081        let response_array = Value::Array(vec![Value::Bin(request_id.to_vec()), response_value]);
1082        let response_plaintext = msgpack::pack(&response_array);
1083
1084        let mut actions = Vec::new();
1085        if let Some(link) = self.links.get(link_id) {
1086            if let Ok(encrypted) = link.engine.encrypt(&response_plaintext, rng) {
1087                let flags = PacketFlags {
1088                    header_type: constants::HEADER_1,
1089                    context_flag: constants::FLAG_UNSET,
1090                    transport_type: constants::TRANSPORT_BROADCAST,
1091                    destination_type: constants::DESTINATION_LINK,
1092                    packet_type: constants::PACKET_TYPE_DATA,
1093                };
1094                if let Ok(pkt) = RawPacket::pack(
1095                    flags,
1096                    0,
1097                    link_id,
1098                    None,
1099                    constants::CONTEXT_RESPONSE,
1100                    &encrypted,
1101                ) {
1102                    actions.push(LinkManagerAction::SendPacket {
1103                        raw: pkt.raw,
1104                        dest_type: constants::DESTINATION_LINK,
1105                        attached_interface: None,
1106                    });
1107                }
1108            }
1109        }
1110        actions
1111    }
1112
1113    /// Send a management response on a link.
1114    /// Called by the driver after building the response for a ManagementRequest.
1115    pub fn send_management_response(
1116        &self,
1117        link_id: &LinkId,
1118        request_id: &[u8; 16],
1119        response_data: &[u8],
1120        rng: &mut dyn Rng,
1121    ) -> Vec<LinkManagerAction> {
1122        self.build_response_packet(link_id, request_id, response_data, rng)
1123    }
1124
1125    /// Send a request on a link.
1126    ///
1127    /// `data` is the msgpack-encoded request data value (e.g. msgpack([True]) for /status).
1128    ///
1129    /// Uses Python-compatible format: plaintext = msgpack([timestamp, path_hash_bytes, data_value]).
1130    /// Returns actions (the encrypted request packet). The response will arrive
1131    /// later via handle_local_delivery with CONTEXT_RESPONSE.
1132    pub fn send_request(
1133        &self,
1134        link_id: &LinkId,
1135        path: &str,
1136        data: &[u8],
1137        rng: &mut dyn Rng,
1138    ) -> Vec<LinkManagerAction> {
1139        use rns_core::msgpack::{self, Value};
1140
1141        let link = match self.links.get(link_id) {
1142            Some(l) => l,
1143            None => return Vec::new(),
1144        };
1145
1146        if link.engine.state() != LinkState::Active {
1147            return Vec::new();
1148        }
1149
1150        let path_hash = compute_path_hash(path);
1151
1152        // Decode data bytes to msgpack Value (or use Bin if can't decode)
1153        let data_value = msgpack::unpack_exact(data).unwrap_or_else(|_| Value::Bin(data.to_vec()));
1154
1155        // Python-compatible format: msgpack([timestamp, Bin(path_hash), data_value])
1156        let request_array = Value::Array(vec![
1157            Value::Float(time::now()),
1158            Value::Bin(path_hash.to_vec()),
1159            data_value,
1160        ]);
1161        let plaintext = msgpack::pack(&request_array);
1162
1163        let encrypted = match link.engine.encrypt(&plaintext, rng) {
1164            Ok(e) => e,
1165            Err(_) => return Vec::new(),
1166        };
1167
1168        let flags = PacketFlags {
1169            header_type: constants::HEADER_1,
1170            context_flag: constants::FLAG_UNSET,
1171            transport_type: constants::TRANSPORT_BROADCAST,
1172            destination_type: constants::DESTINATION_LINK,
1173            packet_type: constants::PACKET_TYPE_DATA,
1174        };
1175
1176        let mut actions = Vec::new();
1177        if let Ok(pkt) = RawPacket::pack(
1178            flags,
1179            0,
1180            link_id,
1181            None,
1182            constants::CONTEXT_REQUEST,
1183            &encrypted,
1184        ) {
1185            actions.push(LinkManagerAction::SendPacket {
1186                raw: pkt.raw,
1187                dest_type: constants::DESTINATION_LINK,
1188                attached_interface: None,
1189            });
1190        }
1191        actions
1192    }
1193
1194    /// Send encrypted data on a link with a given context.
1195    pub fn send_on_link(
1196        &self,
1197        link_id: &LinkId,
1198        plaintext: &[u8],
1199        context: u8,
1200        rng: &mut dyn Rng,
1201    ) -> Vec<LinkManagerAction> {
1202        let link = match self.links.get(link_id) {
1203            Some(l) => l,
1204            None => return Vec::new(),
1205        };
1206
1207        if link.engine.state() != LinkState::Active {
1208            return Vec::new();
1209        }
1210
1211        let encrypted = match link.engine.encrypt(plaintext, rng) {
1212            Ok(e) => e,
1213            Err(_) => return Vec::new(),
1214        };
1215
1216        let flags = PacketFlags {
1217            header_type: constants::HEADER_1,
1218            context_flag: constants::FLAG_UNSET,
1219            transport_type: constants::TRANSPORT_BROADCAST,
1220            destination_type: constants::DESTINATION_LINK,
1221            packet_type: constants::PACKET_TYPE_DATA,
1222        };
1223
1224        let mut actions = Vec::new();
1225        if let Ok(pkt) = RawPacket::pack(flags, 0, link_id, None, context, &encrypted) {
1226            actions.push(LinkManagerAction::SendPacket {
1227                raw: pkt.raw,
1228                dest_type: constants::DESTINATION_LINK,
1229                attached_interface: None,
1230            });
1231        }
1232        actions
1233    }
1234
1235    /// Send an identify message on a link (initiator reveals identity to responder).
1236    pub fn identify(
1237        &self,
1238        link_id: &LinkId,
1239        identity: &rns_crypto::identity::Identity,
1240        rng: &mut dyn Rng,
1241    ) -> Vec<LinkManagerAction> {
1242        let link = match self.links.get(link_id) {
1243            Some(l) => l,
1244            None => return Vec::new(),
1245        };
1246
1247        let encrypted = match link.engine.build_identify(identity, rng) {
1248            Ok(e) => e,
1249            Err(_) => return Vec::new(),
1250        };
1251
1252        let flags = PacketFlags {
1253            header_type: constants::HEADER_1,
1254            context_flag: constants::FLAG_UNSET,
1255            transport_type: constants::TRANSPORT_BROADCAST,
1256            destination_type: constants::DESTINATION_LINK,
1257            packet_type: constants::PACKET_TYPE_DATA,
1258        };
1259
1260        let mut actions = Vec::new();
1261        if let Ok(pkt) = RawPacket::pack(
1262            flags,
1263            0,
1264            link_id,
1265            None,
1266            constants::CONTEXT_LINKIDENTIFY,
1267            &encrypted,
1268        ) {
1269            actions.push(LinkManagerAction::SendPacket {
1270                raw: pkt.raw,
1271                dest_type: constants::DESTINATION_LINK,
1272                attached_interface: None,
1273            });
1274        }
1275        actions
1276    }
1277
1278    /// Tear down a link.
1279    pub fn teardown_link(&mut self, link_id: &LinkId) -> Vec<LinkManagerAction> {
1280        let link = match self.links.get_mut(link_id) {
1281            Some(l) => l,
1282            None => return Vec::new(),
1283        };
1284
1285        let teardown_actions = link.engine.teardown();
1286        if let Some(ref mut channel) = link.channel {
1287            channel.shutdown();
1288        }
1289
1290        let mut actions = self.process_link_actions(link_id, &teardown_actions);
1291
1292        // Send LINKCLOSE packet
1293        let flags = PacketFlags {
1294            header_type: constants::HEADER_1,
1295            context_flag: constants::FLAG_UNSET,
1296            transport_type: constants::TRANSPORT_BROADCAST,
1297            destination_type: constants::DESTINATION_LINK,
1298            packet_type: constants::PACKET_TYPE_DATA,
1299        };
1300        if let Ok(pkt) = RawPacket::pack(flags, 0, link_id, None, constants::CONTEXT_LINKCLOSE, &[])
1301        {
1302            actions.push(LinkManagerAction::SendPacket {
1303                raw: pkt.raw,
1304                dest_type: constants::DESTINATION_LINK,
1305                attached_interface: None,
1306            });
1307        }
1308
1309        actions
1310    }
1311
1312    /// Handle a response on a link.
1313    fn handle_response(&self, link_id: &LinkId, plaintext: &[u8]) -> Vec<LinkManagerAction> {
1314        use rns_core::msgpack;
1315
1316        // Python-compatible response: msgpack([Bin(request_id), response_value])
1317        let arr = match msgpack::unpack_exact(plaintext) {
1318            Ok(msgpack::Value::Array(arr)) if arr.len() >= 2 => arr,
1319            _ => return Vec::new(),
1320        };
1321
1322        let request_id_bytes = match &arr[0] {
1323            msgpack::Value::Bin(b) if b.len() == 16 => b,
1324            _ => return Vec::new(),
1325        };
1326        let mut request_id = [0u8; 16];
1327        request_id.copy_from_slice(request_id_bytes);
1328
1329        let response_data = msgpack::pack(&arr[1]);
1330
1331        vec![LinkManagerAction::ResponseReceived {
1332            link_id: *link_id,
1333            request_id,
1334            data: response_data,
1335        }]
1336    }
1337
1338    /// Handle resource advertisement (CONTEXT_RESOURCE_ADV).
1339    fn handle_resource_adv(
1340        &mut self,
1341        link_id: &LinkId,
1342        adv_plaintext: &[u8],
1343        rng: &mut dyn Rng,
1344    ) -> Vec<LinkManagerAction> {
1345        let link = match self.links.get_mut(link_id) {
1346            Some(l) => l,
1347            None => return Vec::new(),
1348        };
1349
1350        let link_rtt = link.engine.rtt().unwrap_or(1.0);
1351        let now = time::now();
1352
1353        let receiver = match ResourceReceiver::from_advertisement(
1354            adv_plaintext,
1355            constants::RESOURCE_SDU,
1356            link_rtt,
1357            now,
1358            None,
1359            None,
1360        ) {
1361            Ok(r) => r,
1362            Err(e) => {
1363                log::debug!("Resource ADV rejected: {}", e);
1364                return Vec::new();
1365            }
1366        };
1367
1368        let strategy = link.resource_strategy;
1369        let resource_hash = receiver.resource_hash.clone();
1370        let transfer_size = receiver.transfer_size;
1371        let has_metadata = receiver.has_metadata;
1372
1373        match strategy {
1374            ResourceStrategy::AcceptNone => {
1375                // Reject: send RCL
1376                let reject_actions = {
1377                    let mut r = receiver;
1378                    r.reject()
1379                };
1380                self.process_resource_actions(link_id, reject_actions, rng)
1381            }
1382            ResourceStrategy::AcceptAll => {
1383                link.incoming_resources.push(receiver);
1384                let idx = link.incoming_resources.len() - 1;
1385                let resource_actions = link.incoming_resources[idx].accept(now);
1386                let _ = link;
1387                self.process_resource_actions(link_id, resource_actions, rng)
1388            }
1389            ResourceStrategy::AcceptApp => {
1390                link.incoming_resources.push(receiver);
1391                // Query application callback
1392                vec![LinkManagerAction::ResourceAcceptQuery {
1393                    link_id: *link_id,
1394                    resource_hash,
1395                    transfer_size,
1396                    has_metadata,
1397                }]
1398            }
1399        }
1400    }
1401
1402    /// Accept or reject a pending resource (for AcceptApp strategy).
1403    pub fn accept_resource(
1404        &mut self,
1405        link_id: &LinkId,
1406        resource_hash: &[u8],
1407        accept: bool,
1408        rng: &mut dyn Rng,
1409    ) -> Vec<LinkManagerAction> {
1410        let link = match self.links.get_mut(link_id) {
1411            Some(l) => l,
1412            None => return Vec::new(),
1413        };
1414
1415        let now = time::now();
1416        let idx = link
1417            .incoming_resources
1418            .iter()
1419            .position(|r| r.resource_hash == resource_hash);
1420        let idx = match idx {
1421            Some(i) => i,
1422            None => return Vec::new(),
1423        };
1424
1425        let resource_actions = if accept {
1426            link.incoming_resources[idx].accept(now)
1427        } else {
1428            link.incoming_resources[idx].reject()
1429        };
1430
1431        let _ = link;
1432        self.process_resource_actions(link_id, resource_actions, rng)
1433    }
1434
1435    /// Handle resource request (CONTEXT_RESOURCE_REQ) — feed to sender.
1436    fn handle_resource_req(
1437        &mut self,
1438        link_id: &LinkId,
1439        plaintext: &[u8],
1440        rng: &mut dyn Rng,
1441    ) -> Vec<LinkManagerAction> {
1442        let link = match self.links.get_mut(link_id) {
1443            Some(l) => l,
1444            None => return Vec::new(),
1445        };
1446
1447        let now = time::now();
1448        let mut all_actions = Vec::new();
1449        for sender in &mut link.outgoing_resources {
1450            let resource_actions = sender.handle_request(plaintext, now);
1451            if !resource_actions.is_empty() {
1452                all_actions.extend(resource_actions);
1453                break;
1454            }
1455        }
1456
1457        let _ = link;
1458        self.process_resource_actions(link_id, all_actions, rng)
1459    }
1460
1461    /// Handle resource HMU (CONTEXT_RESOURCE_HMU) — feed to receiver.
1462    fn handle_resource_hmu(
1463        &mut self,
1464        link_id: &LinkId,
1465        plaintext: &[u8],
1466        rng: &mut dyn Rng,
1467    ) -> Vec<LinkManagerAction> {
1468        let link = match self.links.get_mut(link_id) {
1469            Some(l) => l,
1470            None => return Vec::new(),
1471        };
1472
1473        let now = time::now();
1474        let mut all_actions = Vec::new();
1475        for receiver in &mut link.incoming_resources {
1476            let resource_actions = receiver.handle_hashmap_update(plaintext, now);
1477            if !resource_actions.is_empty() {
1478                all_actions.extend(resource_actions);
1479                break;
1480            }
1481        }
1482
1483        let _ = link;
1484        self.process_resource_actions(link_id, all_actions, rng)
1485    }
1486
1487    /// Handle resource part (CONTEXT_RESOURCE) — feed raw to receiver.
1488    fn handle_resource_part(
1489        &mut self,
1490        link_id: &LinkId,
1491        raw_data: &[u8],
1492        rng: &mut dyn Rng,
1493    ) -> Vec<LinkManagerAction> {
1494        let link = match self.links.get_mut(link_id) {
1495            Some(l) => l,
1496            None => return Vec::new(),
1497        };
1498
1499        let now = time::now();
1500        let mut all_actions = Vec::new();
1501        let mut assemble_idx = None;
1502
1503        for (idx, receiver) in link.incoming_resources.iter_mut().enumerate() {
1504            let resource_actions = receiver.receive_part(raw_data, now);
1505            if !resource_actions.is_empty() {
1506                // Check if all parts received (triggers assembly)
1507                if receiver.received_count == receiver.total_parts {
1508                    assemble_idx = Some(idx);
1509                }
1510                all_actions.extend(resource_actions);
1511                break;
1512            }
1513        }
1514
1515        // Assemble if all parts received
1516        if let Some(idx) = assemble_idx {
1517            let decrypt_fn = |ciphertext: &[u8]| -> Result<Vec<u8>, ()> {
1518                link.engine.decrypt(ciphertext).map_err(|_| ())
1519            };
1520            let assemble_actions =
1521                link.incoming_resources[idx].assemble(&decrypt_fn, &Bzip2Compressor);
1522            all_actions.extend(assemble_actions);
1523        }
1524
1525        let _ = link;
1526        self.process_resource_actions(link_id, all_actions, rng)
1527    }
1528
1529    /// Handle resource proof (CONTEXT_RESOURCE_PRF) — feed to sender.
1530    fn handle_resource_prf(
1531        &mut self,
1532        link_id: &LinkId,
1533        plaintext: &[u8],
1534    ) -> Vec<LinkManagerAction> {
1535        let link = match self.links.get_mut(link_id) {
1536            Some(l) => l,
1537            None => return Vec::new(),
1538        };
1539
1540        let now = time::now();
1541        let mut result_actions = Vec::new();
1542        for sender in &mut link.outgoing_resources {
1543            let resource_actions = sender.handle_proof(plaintext, now);
1544            if !resource_actions.is_empty() {
1545                result_actions.extend(resource_actions);
1546                break;
1547            }
1548        }
1549
1550        // Convert to LinkManagerActions
1551        let mut actions = Vec::new();
1552        for ra in result_actions {
1553            match ra {
1554                ResourceAction::Completed => {
1555                    actions.push(LinkManagerAction::ResourceCompleted { link_id: *link_id });
1556                }
1557                ResourceAction::Failed(e) => {
1558                    actions.push(LinkManagerAction::ResourceFailed {
1559                        link_id: *link_id,
1560                        error: format!("{}", e),
1561                    });
1562                }
1563                _ => {}
1564            }
1565        }
1566
1567        // Clean up completed/failed senders
1568        link.outgoing_resources
1569            .retain(|s| s.status < rns_core::resource::ResourceStatus::Complete);
1570
1571        actions
1572    }
1573
1574    /// Handle cancel from initiator (CONTEXT_RESOURCE_ICL).
1575    fn handle_resource_icl(&mut self, link_id: &LinkId) -> Vec<LinkManagerAction> {
1576        let link = match self.links.get_mut(link_id) {
1577            Some(l) => l,
1578            None => return Vec::new(),
1579        };
1580
1581        let mut actions = Vec::new();
1582        for receiver in &mut link.incoming_resources {
1583            let ra = receiver.handle_cancel();
1584            for a in ra {
1585                if let ResourceAction::Failed(ref e) = a {
1586                    actions.push(LinkManagerAction::ResourceFailed {
1587                        link_id: *link_id,
1588                        error: format!("{}", e),
1589                    });
1590                }
1591            }
1592        }
1593        link.incoming_resources
1594            .retain(|r| r.status < rns_core::resource::ResourceStatus::Complete);
1595        actions
1596    }
1597
1598    /// Handle cancel from receiver (CONTEXT_RESOURCE_RCL).
1599    fn handle_resource_rcl(&mut self, link_id: &LinkId) -> Vec<LinkManagerAction> {
1600        let link = match self.links.get_mut(link_id) {
1601            Some(l) => l,
1602            None => return Vec::new(),
1603        };
1604
1605        let mut actions = Vec::new();
1606        for sender in &mut link.outgoing_resources {
1607            let ra = sender.handle_reject();
1608            for a in ra {
1609                if let ResourceAction::Failed(ref e) = a {
1610                    actions.push(LinkManagerAction::ResourceFailed {
1611                        link_id: *link_id,
1612                        error: format!("{}", e),
1613                    });
1614                }
1615            }
1616        }
1617        link.outgoing_resources
1618            .retain(|s| s.status < rns_core::resource::ResourceStatus::Complete);
1619        actions
1620    }
1621
1622    /// Convert ResourceActions to LinkManagerActions.
1623    fn process_resource_actions(
1624        &self,
1625        link_id: &LinkId,
1626        actions: Vec<ResourceAction>,
1627        rng: &mut dyn Rng,
1628    ) -> Vec<LinkManagerAction> {
1629        let link = match self.links.get(link_id) {
1630            Some(l) => l,
1631            None => return Vec::new(),
1632        };
1633
1634        let mut result = Vec::new();
1635        for action in actions {
1636            match action {
1637                ResourceAction::SendAdvertisement(data) => {
1638                    // Link-encrypt and send as CONTEXT_RESOURCE_ADV
1639                    if let Ok(encrypted) = link.engine.encrypt(&data, rng) {
1640                        result.extend(self.build_link_packet(
1641                            link_id,
1642                            constants::CONTEXT_RESOURCE_ADV,
1643                            &encrypted,
1644                        ));
1645                    }
1646                }
1647                ResourceAction::SendPart(data) => {
1648                    // Parts are NOT link-encrypted — send raw as CONTEXT_RESOURCE
1649                    result.extend(self.build_link_packet(
1650                        link_id,
1651                        constants::CONTEXT_RESOURCE,
1652                        &data,
1653                    ));
1654                }
1655                ResourceAction::SendRequest(data) => {
1656                    if let Ok(encrypted) = link.engine.encrypt(&data, rng) {
1657                        result.extend(self.build_link_packet(
1658                            link_id,
1659                            constants::CONTEXT_RESOURCE_REQ,
1660                            &encrypted,
1661                        ));
1662                    }
1663                }
1664                ResourceAction::SendHmu(data) => {
1665                    if let Ok(encrypted) = link.engine.encrypt(&data, rng) {
1666                        result.extend(self.build_link_packet(
1667                            link_id,
1668                            constants::CONTEXT_RESOURCE_HMU,
1669                            &encrypted,
1670                        ));
1671                    }
1672                }
1673                ResourceAction::SendProof(data) => {
1674                    if let Ok(encrypted) = link.engine.encrypt(&data, rng) {
1675                        result.extend(self.build_link_packet(
1676                            link_id,
1677                            constants::CONTEXT_RESOURCE_PRF,
1678                            &encrypted,
1679                        ));
1680                    }
1681                }
1682                ResourceAction::SendCancelInitiator(data) => {
1683                    if let Ok(encrypted) = link.engine.encrypt(&data, rng) {
1684                        result.extend(self.build_link_packet(
1685                            link_id,
1686                            constants::CONTEXT_RESOURCE_ICL,
1687                            &encrypted,
1688                        ));
1689                    }
1690                }
1691                ResourceAction::SendCancelReceiver(data) => {
1692                    if let Ok(encrypted) = link.engine.encrypt(&data, rng) {
1693                        result.extend(self.build_link_packet(
1694                            link_id,
1695                            constants::CONTEXT_RESOURCE_RCL,
1696                            &encrypted,
1697                        ));
1698                    }
1699                }
1700                ResourceAction::DataReceived { data, metadata } => {
1701                    result.push(LinkManagerAction::ResourceReceived {
1702                        link_id: *link_id,
1703                        data,
1704                        metadata,
1705                    });
1706                }
1707                ResourceAction::Completed => {
1708                    result.push(LinkManagerAction::ResourceCompleted { link_id: *link_id });
1709                }
1710                ResourceAction::Failed(e) => {
1711                    result.push(LinkManagerAction::ResourceFailed {
1712                        link_id: *link_id,
1713                        error: format!("{}", e),
1714                    });
1715                }
1716                ResourceAction::ProgressUpdate { received, total } => {
1717                    result.push(LinkManagerAction::ResourceProgress {
1718                        link_id: *link_id,
1719                        received,
1720                        total,
1721                    });
1722                }
1723            }
1724        }
1725        result
1726    }
1727
1728    /// Build a link DATA packet with a given context and data.
1729    fn build_link_packet(
1730        &self,
1731        link_id: &LinkId,
1732        context: u8,
1733        data: &[u8],
1734    ) -> Vec<LinkManagerAction> {
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        let mut actions = Vec::new();
1743        if let Ok(pkt) = RawPacket::pack(flags, 0, link_id, None, context, data) {
1744            actions.push(LinkManagerAction::SendPacket {
1745                raw: pkt.raw,
1746                dest_type: constants::DESTINATION_LINK,
1747                attached_interface: None,
1748            });
1749        }
1750        actions
1751    }
1752
1753    /// Start sending a resource on a link.
1754    pub fn send_resource(
1755        &mut self,
1756        link_id: &LinkId,
1757        data: &[u8],
1758        metadata: Option<&[u8]>,
1759        rng: &mut dyn Rng,
1760    ) -> Vec<LinkManagerAction> {
1761        let link = match self.links.get_mut(link_id) {
1762            Some(l) => l,
1763            None => return Vec::new(),
1764        };
1765
1766        if link.engine.state() != LinkState::Active {
1767            return Vec::new();
1768        }
1769
1770        let link_rtt = link.engine.rtt().unwrap_or(1.0);
1771        let now = time::now();
1772
1773        // Use RefCell for interior mutability since ResourceSender::new expects &dyn Fn (not FnMut)
1774        // but link.engine.encrypt needs &mut dyn Rng
1775        let enc_rng = std::cell::RefCell::new(rns_crypto::OsRng);
1776        let encrypt_fn = |plaintext: &[u8]| -> Vec<u8> {
1777            link.engine
1778                .encrypt(plaintext, &mut *enc_rng.borrow_mut())
1779                .unwrap_or_else(|_| plaintext.to_vec())
1780        };
1781
1782        let sender = match ResourceSender::new(
1783            data,
1784            metadata,
1785            constants::RESOURCE_SDU,
1786            &encrypt_fn,
1787            &Bzip2Compressor,
1788            rng,
1789            now,
1790            true,  // auto_compress
1791            false, // is_response
1792            None,  // request_id
1793            1,     // segment_index
1794            1,     // total_segments
1795            None,  // original_hash
1796            link_rtt,
1797            6.0, // traffic_timeout_factor
1798        ) {
1799            Ok(s) => s,
1800            Err(e) => {
1801                log::debug!("Failed to create ResourceSender: {}", e);
1802                return Vec::new();
1803            }
1804        };
1805
1806        let mut sender = sender;
1807        let adv_actions = sender.advertise(now);
1808        link.outgoing_resources.push(sender);
1809
1810        let _ = link;
1811        self.process_resource_actions(link_id, adv_actions, rng)
1812    }
1813
1814    /// Set the resource acceptance strategy for a link.
1815    pub fn set_resource_strategy(&mut self, link_id: &LinkId, strategy: ResourceStrategy) {
1816        if let Some(link) = self.links.get_mut(link_id) {
1817            link.resource_strategy = strategy;
1818        }
1819    }
1820
1821    /// Flush the channel TX ring for a link, clearing outstanding messages.
1822    /// Called after holepunch completion where signaling messages are fire-and-forget.
1823    pub fn flush_channel_tx(&mut self, link_id: &LinkId) {
1824        if let Some(link) = self.links.get_mut(link_id) {
1825            if let Some(ref mut channel) = link.channel {
1826                channel.flush_tx();
1827            }
1828        }
1829    }
1830
1831    /// Send a channel message on a link.
1832    pub fn send_channel_message(
1833        &mut self,
1834        link_id: &LinkId,
1835        msgtype: u16,
1836        payload: &[u8],
1837        rng: &mut dyn Rng,
1838    ) -> Vec<LinkManagerAction> {
1839        let link = match self.links.get_mut(link_id) {
1840            Some(l) => l,
1841            None => return Vec::new(),
1842        };
1843
1844        let channel = match link.channel {
1845            Some(ref mut ch) => ch,
1846            None => return Vec::new(),
1847        };
1848
1849        let link_mdu = constants::MDU; // Use MDU as approximate link MDU
1850        let now = time::now();
1851        let chan_actions = match channel.send(msgtype, payload, now, link_mdu) {
1852            Ok(a) => a,
1853            Err(e) => {
1854                log::debug!("Channel send failed: {:?}", e);
1855                return Vec::new();
1856            }
1857        };
1858
1859        let _ = link;
1860        self.process_channel_actions(link_id, chan_actions, rng)
1861    }
1862
1863    /// Periodic tick: check keepalive, stale, timeouts for all links.
1864    pub fn tick(&mut self, rng: &mut dyn Rng) -> Vec<LinkManagerAction> {
1865        let now = time::now();
1866        let mut all_actions = Vec::new();
1867
1868        // Collect link_ids to avoid borrow issues
1869        let link_ids: Vec<LinkId> = self.links.keys().copied().collect();
1870
1871        for link_id in &link_ids {
1872            let link = match self.links.get_mut(link_id) {
1873                Some(l) => l,
1874                None => continue,
1875            };
1876
1877            // Tick the engine
1878            let tick_actions = link.engine.tick(now);
1879            all_actions.extend(self.process_link_actions(link_id, &tick_actions));
1880
1881            // Check if keepalive is needed
1882            let link = match self.links.get_mut(link_id) {
1883                Some(l) => l,
1884                None => continue,
1885            };
1886            if link.engine.needs_keepalive(now) {
1887                // Send keepalive packet (empty data with CONTEXT_KEEPALIVE)
1888                let flags = PacketFlags {
1889                    header_type: constants::HEADER_1,
1890                    context_flag: constants::FLAG_UNSET,
1891                    transport_type: constants::TRANSPORT_BROADCAST,
1892                    destination_type: constants::DESTINATION_LINK,
1893                    packet_type: constants::PACKET_TYPE_DATA,
1894                };
1895                if let Ok(pkt) =
1896                    RawPacket::pack(flags, 0, link_id, None, constants::CONTEXT_KEEPALIVE, &[])
1897                {
1898                    all_actions.push(LinkManagerAction::SendPacket {
1899                        raw: pkt.raw,
1900                        dest_type: constants::DESTINATION_LINK,
1901                        attached_interface: None,
1902                    });
1903                    link.engine.record_outbound(now, true);
1904                }
1905            }
1906        }
1907
1908        // Tick resource senders and receivers
1909        for link_id in &link_ids {
1910            let link = match self.links.get_mut(link_id) {
1911                Some(l) => l,
1912                None => continue,
1913            };
1914
1915            // Tick outgoing resources (senders)
1916            let mut sender_actions = Vec::new();
1917            for sender in &mut link.outgoing_resources {
1918                sender_actions.extend(sender.tick(now));
1919            }
1920
1921            // Tick incoming resources (receivers)
1922            let mut receiver_actions = Vec::new();
1923            for receiver in &mut link.incoming_resources {
1924                let decrypt_fn = |ciphertext: &[u8]| -> Result<Vec<u8>, ()> {
1925                    link.engine.decrypt(ciphertext).map_err(|_| ())
1926                };
1927                receiver_actions.extend(receiver.tick(now, &decrypt_fn, &Bzip2Compressor));
1928            }
1929
1930            // Clean up completed/failed resources
1931            link.outgoing_resources
1932                .retain(|s| s.status < rns_core::resource::ResourceStatus::Complete);
1933            link.incoming_resources
1934                .retain(|r| r.status < rns_core::resource::ResourceStatus::Assembling);
1935
1936            let _ = link;
1937            all_actions.extend(self.process_resource_actions(link_id, sender_actions, rng));
1938            all_actions.extend(self.process_resource_actions(link_id, receiver_actions, rng));
1939        }
1940
1941        // Clean up closed links
1942        let closed: Vec<LinkId> = self
1943            .links
1944            .iter()
1945            .filter(|(_, l)| l.engine.state() == LinkState::Closed)
1946            .map(|(id, _)| *id)
1947            .collect();
1948        for id in closed {
1949            self.links.remove(&id);
1950            all_actions.push(LinkManagerAction::DeregisterLinkDest { link_id: id });
1951        }
1952
1953        all_actions
1954    }
1955
1956    /// Check if a destination hash is a known link_id managed by this manager.
1957    pub fn is_link_destination(&self, dest_hash: &[u8; 16]) -> bool {
1958        self.links.contains_key(dest_hash) || self.link_destinations.contains_key(dest_hash)
1959    }
1960
1961    /// Get the state of a link.
1962    pub fn link_state(&self, link_id: &LinkId) -> Option<LinkState> {
1963        self.links.get(link_id).map(|l| l.engine.state())
1964    }
1965
1966    /// Get the RTT of a link.
1967    pub fn link_rtt(&self, link_id: &LinkId) -> Option<f64> {
1968        self.links.get(link_id).and_then(|l| l.engine.rtt())
1969    }
1970
1971    /// Update the RTT of a link (e.g., after path redirect to a direct connection).
1972    pub fn set_link_rtt(&mut self, link_id: &LinkId, rtt: f64) {
1973        if let Some(link) = self.links.get_mut(link_id) {
1974            link.engine.set_rtt(rtt);
1975        }
1976    }
1977
1978    /// Reset the inbound timer for a link (e.g., after path redirect).
1979    pub fn record_link_inbound(&mut self, link_id: &LinkId) {
1980        if let Some(link) = self.links.get_mut(link_id) {
1981            link.engine.record_inbound(time::now());
1982        }
1983    }
1984
1985    /// Update the MTU of a link (e.g., after path redirect to a different interface).
1986    pub fn set_link_mtu(&mut self, link_id: &LinkId, mtu: u32) {
1987        if let Some(link) = self.links.get_mut(link_id) {
1988            link.engine.set_mtu(mtu);
1989        }
1990    }
1991
1992    /// Get the number of active links.
1993    pub fn link_count(&self) -> usize {
1994        self.links.len()
1995    }
1996
1997    /// Get information about all active links.
1998    pub fn link_entries(&self) -> Vec<crate::event::LinkInfoEntry> {
1999        self.links
2000            .iter()
2001            .map(|(link_id, managed)| {
2002                let state = match managed.engine.state() {
2003                    LinkState::Pending => "pending",
2004                    LinkState::Handshake => "handshake",
2005                    LinkState::Active => "active",
2006                    LinkState::Stale => "stale",
2007                    LinkState::Closed => "closed",
2008                };
2009                crate::event::LinkInfoEntry {
2010                    link_id: *link_id,
2011                    state: state.to_string(),
2012                    is_initiator: managed.engine.is_initiator(),
2013                    dest_hash: managed.dest_hash,
2014                    remote_identity: managed.remote_identity.as_ref().map(|(h, _)| *h),
2015                    rtt: managed.engine.rtt(),
2016                }
2017            })
2018            .collect()
2019    }
2020
2021    /// Get information about all active resource transfers.
2022    pub fn resource_entries(&self) -> Vec<crate::event::ResourceInfoEntry> {
2023        let mut entries = Vec::new();
2024        for (link_id, managed) in &self.links {
2025            for recv in &managed.incoming_resources {
2026                let (received, total) = recv.progress();
2027                entries.push(crate::event::ResourceInfoEntry {
2028                    link_id: *link_id,
2029                    direction: "incoming".to_string(),
2030                    total_parts: total,
2031                    transferred_parts: received,
2032                    complete: received >= total && total > 0,
2033                });
2034            }
2035            for send in &managed.outgoing_resources {
2036                let total = send.total_parts();
2037                let sent = send.sent_parts;
2038                entries.push(crate::event::ResourceInfoEntry {
2039                    link_id: *link_id,
2040                    direction: "outgoing".to_string(),
2041                    total_parts: total,
2042                    transferred_parts: sent,
2043                    complete: sent >= total && total > 0,
2044                });
2045            }
2046        }
2047        entries
2048    }
2049
2050    /// Convert LinkActions to LinkManagerActions.
2051    fn process_link_actions(
2052        &self,
2053        link_id: &LinkId,
2054        actions: &[LinkAction],
2055    ) -> Vec<LinkManagerAction> {
2056        let mut result = Vec::new();
2057        for action in actions {
2058            match action {
2059                LinkAction::StateChanged {
2060                    new_state, reason, ..
2061                } => match new_state {
2062                    LinkState::Closed => {
2063                        result.push(LinkManagerAction::LinkClosed {
2064                            link_id: *link_id,
2065                            reason: *reason,
2066                        });
2067                    }
2068                    _ => {}
2069                },
2070                LinkAction::LinkEstablished {
2071                    rtt, is_initiator, ..
2072                } => {
2073                    let dest_hash = self
2074                        .links
2075                        .get(link_id)
2076                        .map(|l| l.dest_hash)
2077                        .unwrap_or([0u8; 16]);
2078                    result.push(LinkManagerAction::LinkEstablished {
2079                        link_id: *link_id,
2080                        dest_hash,
2081                        rtt: *rtt,
2082                        is_initiator: *is_initiator,
2083                    });
2084                }
2085                LinkAction::RemoteIdentified {
2086                    identity_hash,
2087                    public_key,
2088                    ..
2089                } => {
2090                    result.push(LinkManagerAction::RemoteIdentified {
2091                        link_id: *link_id,
2092                        identity_hash: *identity_hash,
2093                        public_key: *public_key,
2094                    });
2095                }
2096                LinkAction::DataReceived { .. } => {
2097                    // Data delivery is handled at a higher level
2098                }
2099            }
2100        }
2101        result
2102    }
2103
2104    /// Convert ChannelActions to LinkManagerActions.
2105    fn process_channel_actions(
2106        &self,
2107        link_id: &LinkId,
2108        actions: Vec<rns_core::channel::ChannelAction>,
2109        rng: &mut dyn Rng,
2110    ) -> Vec<LinkManagerAction> {
2111        let mut result = Vec::new();
2112        for action in actions {
2113            match action {
2114                rns_core::channel::ChannelAction::SendOnLink { raw } => {
2115                    // Encrypt and send as CHANNEL context
2116                    if let Some(link) = self.links.get(link_id) {
2117                        if let Ok(encrypted) = link.engine.encrypt(&raw, rng) {
2118                            let flags = PacketFlags {
2119                                header_type: constants::HEADER_1,
2120                                context_flag: constants::FLAG_UNSET,
2121                                transport_type: constants::TRANSPORT_BROADCAST,
2122                                destination_type: constants::DESTINATION_LINK,
2123                                packet_type: constants::PACKET_TYPE_DATA,
2124                            };
2125                            if let Ok(pkt) = RawPacket::pack(
2126                                flags,
2127                                0,
2128                                link_id,
2129                                None,
2130                                constants::CONTEXT_CHANNEL,
2131                                &encrypted,
2132                            ) {
2133                                result.push(LinkManagerAction::SendPacket {
2134                                    raw: pkt.raw,
2135                                    dest_type: constants::DESTINATION_LINK,
2136                                    attached_interface: None,
2137                                });
2138                            }
2139                        }
2140                    }
2141                }
2142                rns_core::channel::ChannelAction::MessageReceived {
2143                    msgtype, payload, ..
2144                } => {
2145                    result.push(LinkManagerAction::ChannelMessageReceived {
2146                        link_id: *link_id,
2147                        msgtype,
2148                        payload,
2149                    });
2150                }
2151                rns_core::channel::ChannelAction::TeardownLink => {
2152                    result.push(LinkManagerAction::LinkClosed {
2153                        link_id: *link_id,
2154                        reason: Some(TeardownReason::Timeout),
2155                    });
2156                }
2157            }
2158        }
2159        result
2160    }
2161}
2162
2163/// Compute a path hash from a path string.
2164/// Uses truncated SHA-256 (first 16 bytes).
2165fn compute_path_hash(path: &str) -> [u8; 16] {
2166    let full = rns_core::hash::full_hash(path.as_bytes());
2167    let mut result = [0u8; 16];
2168    result.copy_from_slice(&full[..16]);
2169    result
2170}
2171
2172#[cfg(test)]
2173mod tests {
2174    use super::*;
2175    use rns_crypto::identity::Identity;
2176    use rns_crypto::{FixedRng, OsRng};
2177
2178    fn make_rng(seed: u8) -> FixedRng {
2179        FixedRng::new(&[seed; 128])
2180    }
2181
2182    fn make_dest_keys(rng: &mut dyn Rng) -> (Ed25519PrivateKey, [u8; 32]) {
2183        let sig_prv = Ed25519PrivateKey::generate(rng);
2184        let sig_pub_bytes = sig_prv.public_key().public_bytes();
2185        (sig_prv, sig_pub_bytes)
2186    }
2187
2188    #[test]
2189    fn test_register_link_destination() {
2190        let mut mgr = LinkManager::new();
2191        let mut rng = make_rng(0x01);
2192        let (sig_prv, sig_pub_bytes) = make_dest_keys(&mut rng);
2193        let dest_hash = [0xDD; 16];
2194
2195        mgr.register_link_destination(
2196            dest_hash,
2197            sig_prv,
2198            sig_pub_bytes,
2199            ResourceStrategy::AcceptNone,
2200        );
2201        assert!(mgr.is_link_destination(&dest_hash));
2202
2203        mgr.deregister_link_destination(&dest_hash);
2204        assert!(!mgr.is_link_destination(&dest_hash));
2205    }
2206
2207    #[test]
2208    fn test_create_link() {
2209        let mut mgr = LinkManager::new();
2210        let mut rng = OsRng;
2211        let dest_hash = [0xDD; 16];
2212
2213        let sig_pub_bytes = [0xAA; 32]; // dummy sig pub for test
2214        let (link_id, actions) = mgr.create_link(
2215            &dest_hash,
2216            &sig_pub_bytes,
2217            1,
2218            constants::MTU as u32,
2219            &mut rng,
2220        );
2221        assert_ne!(link_id, [0u8; 16]);
2222        // Should have RegisterLinkDest + SendPacket
2223        assert_eq!(actions.len(), 2);
2224        assert!(matches!(
2225            actions[0],
2226            LinkManagerAction::RegisterLinkDest { .. }
2227        ));
2228        assert!(matches!(actions[1], LinkManagerAction::SendPacket { .. }));
2229
2230        // Link should be in Pending state
2231        assert_eq!(mgr.link_state(&link_id), Some(LinkState::Pending));
2232    }
2233
2234    #[test]
2235    fn test_full_handshake_via_manager() {
2236        let mut rng = OsRng;
2237        let dest_hash = [0xDD; 16];
2238
2239        // Setup responder
2240        let mut responder_mgr = LinkManager::new();
2241        let (sig_prv, sig_pub_bytes) = make_dest_keys(&mut rng);
2242        responder_mgr.register_link_destination(
2243            dest_hash,
2244            sig_prv,
2245            sig_pub_bytes,
2246            ResourceStrategy::AcceptNone,
2247        );
2248
2249        // Setup initiator
2250        let mut initiator_mgr = LinkManager::new();
2251
2252        // Step 1: Initiator creates link (needs dest signing pub key for LRPROOF verification)
2253        let (link_id, init_actions) = initiator_mgr.create_link(
2254            &dest_hash,
2255            &sig_pub_bytes,
2256            1,
2257            constants::MTU as u32,
2258            &mut rng,
2259        );
2260        assert_eq!(init_actions.len(), 2);
2261
2262        // Extract the LINKREQUEST packet raw bytes
2263        let linkrequest_raw = match &init_actions[1] {
2264            LinkManagerAction::SendPacket { raw, .. } => raw.clone(),
2265            _ => panic!("Expected SendPacket"),
2266        };
2267
2268        // Parse to get packet_hash and dest_hash
2269        let lr_packet = RawPacket::unpack(&linkrequest_raw).unwrap();
2270
2271        // Step 2: Responder handles LINKREQUEST
2272        let resp_actions = responder_mgr.handle_local_delivery(
2273            lr_packet.destination_hash,
2274            &linkrequest_raw,
2275            lr_packet.packet_hash,
2276            rns_core::transport::types::InterfaceId(0),
2277            &mut rng,
2278        );
2279        // Should have RegisterLinkDest + SendPacket(LRPROOF)
2280        assert!(resp_actions.len() >= 2);
2281        assert!(matches!(
2282            resp_actions[0],
2283            LinkManagerAction::RegisterLinkDest { .. }
2284        ));
2285
2286        // Extract LRPROOF packet
2287        let lrproof_raw = match &resp_actions[1] {
2288            LinkManagerAction::SendPacket { raw, .. } => raw.clone(),
2289            _ => panic!("Expected SendPacket for LRPROOF"),
2290        };
2291
2292        // Step 3: Initiator handles LRPROOF
2293        let lrproof_packet = RawPacket::unpack(&lrproof_raw).unwrap();
2294        let init_actions2 = initiator_mgr.handle_local_delivery(
2295            lrproof_packet.destination_hash,
2296            &lrproof_raw,
2297            lrproof_packet.packet_hash,
2298            rns_core::transport::types::InterfaceId(0),
2299            &mut rng,
2300        );
2301
2302        // Should have LinkEstablished + SendPacket(LRRTT)
2303        let has_established = init_actions2
2304            .iter()
2305            .any(|a| matches!(a, LinkManagerAction::LinkEstablished { .. }));
2306        assert!(has_established, "Initiator should emit LinkEstablished");
2307
2308        // Extract LRRTT
2309        let lrrtt_raw = init_actions2
2310            .iter()
2311            .find_map(|a| match a {
2312                LinkManagerAction::SendPacket { raw, .. } => Some(raw.clone()),
2313                _ => None,
2314            })
2315            .expect("Should have LRRTT SendPacket");
2316
2317        // Step 4: Responder handles LRRTT
2318        let lrrtt_packet = RawPacket::unpack(&lrrtt_raw).unwrap();
2319        let resp_link_id = lrrtt_packet.destination_hash;
2320        let resp_actions2 = responder_mgr.handle_local_delivery(
2321            resp_link_id,
2322            &lrrtt_raw,
2323            lrrtt_packet.packet_hash,
2324            rns_core::transport::types::InterfaceId(0),
2325            &mut rng,
2326        );
2327
2328        let has_established = resp_actions2
2329            .iter()
2330            .any(|a| matches!(a, LinkManagerAction::LinkEstablished { .. }));
2331        assert!(has_established, "Responder should emit LinkEstablished");
2332
2333        // Both sides should be Active
2334        assert_eq!(initiator_mgr.link_state(&link_id), Some(LinkState::Active));
2335        assert_eq!(responder_mgr.link_state(&link_id), Some(LinkState::Active));
2336
2337        // Both should have RTT
2338        assert!(initiator_mgr.link_rtt(&link_id).is_some());
2339        assert!(responder_mgr.link_rtt(&link_id).is_some());
2340    }
2341
2342    #[test]
2343    fn test_encrypted_data_exchange() {
2344        let mut rng = OsRng;
2345        let dest_hash = [0xDD; 16];
2346        let mut resp_mgr = LinkManager::new();
2347        let (sig_prv, sig_pub_bytes) = make_dest_keys(&mut rng);
2348        resp_mgr.register_link_destination(
2349            dest_hash,
2350            sig_prv,
2351            sig_pub_bytes,
2352            ResourceStrategy::AcceptNone,
2353        );
2354        let mut init_mgr = LinkManager::new();
2355
2356        // Handshake
2357        let (link_id, init_actions) = init_mgr.create_link(
2358            &dest_hash,
2359            &sig_pub_bytes,
2360            1,
2361            constants::MTU as u32,
2362            &mut rng,
2363        );
2364        let lr_raw = extract_send_packet(&init_actions);
2365        let lr_pkt = RawPacket::unpack(&lr_raw).unwrap();
2366        let resp_actions = resp_mgr.handle_local_delivery(
2367            lr_pkt.destination_hash,
2368            &lr_raw,
2369            lr_pkt.packet_hash,
2370            rns_core::transport::types::InterfaceId(0),
2371            &mut rng,
2372        );
2373        let lrproof_raw = extract_send_packet_at(&resp_actions, 1);
2374        let lrproof_pkt = RawPacket::unpack(&lrproof_raw).unwrap();
2375        let init_actions2 = init_mgr.handle_local_delivery(
2376            lrproof_pkt.destination_hash,
2377            &lrproof_raw,
2378            lrproof_pkt.packet_hash,
2379            rns_core::transport::types::InterfaceId(0),
2380            &mut rng,
2381        );
2382        let lrrtt_raw = extract_any_send_packet(&init_actions2);
2383        let lrrtt_pkt = RawPacket::unpack(&lrrtt_raw).unwrap();
2384        resp_mgr.handle_local_delivery(
2385            lrrtt_pkt.destination_hash,
2386            &lrrtt_raw,
2387            lrrtt_pkt.packet_hash,
2388            rns_core::transport::types::InterfaceId(0),
2389            &mut rng,
2390        );
2391
2392        // Send data from initiator to responder
2393        let actions =
2394            init_mgr.send_on_link(&link_id, b"hello link!", constants::CONTEXT_NONE, &mut rng);
2395        assert_eq!(actions.len(), 1);
2396        assert!(matches!(actions[0], LinkManagerAction::SendPacket { .. }));
2397    }
2398
2399    #[test]
2400    fn test_request_response() {
2401        let mut rng = OsRng;
2402        let dest_hash = [0xDD; 16];
2403        let mut resp_mgr = LinkManager::new();
2404        let (sig_prv, sig_pub_bytes) = make_dest_keys(&mut rng);
2405        resp_mgr.register_link_destination(
2406            dest_hash,
2407            sig_prv,
2408            sig_pub_bytes,
2409            ResourceStrategy::AcceptNone,
2410        );
2411
2412        // Register a request handler
2413        resp_mgr.register_request_handler("/status", None, |_link_id, _path, _data, _remote| {
2414            Some(b"OK".to_vec())
2415        });
2416
2417        let mut init_mgr = LinkManager::new();
2418
2419        // Complete handshake
2420        let (link_id, init_actions) = init_mgr.create_link(
2421            &dest_hash,
2422            &sig_pub_bytes,
2423            1,
2424            constants::MTU as u32,
2425            &mut rng,
2426        );
2427        let lr_raw = extract_send_packet(&init_actions);
2428        let lr_pkt = RawPacket::unpack(&lr_raw).unwrap();
2429        let resp_actions = resp_mgr.handle_local_delivery(
2430            lr_pkt.destination_hash,
2431            &lr_raw,
2432            lr_pkt.packet_hash,
2433            rns_core::transport::types::InterfaceId(0),
2434            &mut rng,
2435        );
2436        let lrproof_raw = extract_send_packet_at(&resp_actions, 1);
2437        let lrproof_pkt = RawPacket::unpack(&lrproof_raw).unwrap();
2438        let init_actions2 = init_mgr.handle_local_delivery(
2439            lrproof_pkt.destination_hash,
2440            &lrproof_raw,
2441            lrproof_pkt.packet_hash,
2442            rns_core::transport::types::InterfaceId(0),
2443            &mut rng,
2444        );
2445        let lrrtt_raw = extract_any_send_packet(&init_actions2);
2446        let lrrtt_pkt = RawPacket::unpack(&lrrtt_raw).unwrap();
2447        resp_mgr.handle_local_delivery(
2448            lrrtt_pkt.destination_hash,
2449            &lrrtt_raw,
2450            lrrtt_pkt.packet_hash,
2451            rns_core::transport::types::InterfaceId(0),
2452            &mut rng,
2453        );
2454
2455        // Send request from initiator
2456        let req_actions = init_mgr.send_request(&link_id, "/status", b"query", &mut rng);
2457        assert_eq!(req_actions.len(), 1);
2458
2459        // Deliver request to responder
2460        let req_raw = extract_send_packet_from(&req_actions);
2461        let req_pkt = RawPacket::unpack(&req_raw).unwrap();
2462        let resp_actions = resp_mgr.handle_local_delivery(
2463            req_pkt.destination_hash,
2464            &req_raw,
2465            req_pkt.packet_hash,
2466            rns_core::transport::types::InterfaceId(0),
2467            &mut rng,
2468        );
2469
2470        // Should have a response SendPacket
2471        let has_response = resp_actions
2472            .iter()
2473            .any(|a| matches!(a, LinkManagerAction::SendPacket { .. }));
2474        assert!(has_response, "Handler should produce a response packet");
2475    }
2476
2477    #[test]
2478    fn test_request_acl_deny_unidentified() {
2479        let mut rng = OsRng;
2480        let dest_hash = [0xDD; 16];
2481        let mut resp_mgr = LinkManager::new();
2482        let (sig_prv, sig_pub_bytes) = make_dest_keys(&mut rng);
2483        resp_mgr.register_link_destination(
2484            dest_hash,
2485            sig_prv,
2486            sig_pub_bytes,
2487            ResourceStrategy::AcceptNone,
2488        );
2489
2490        // Register handler with ACL (only allow specific identity)
2491        resp_mgr.register_request_handler(
2492            "/restricted",
2493            Some(vec![[0xAA; 16]]),
2494            |_link_id, _path, _data, _remote| Some(b"secret".to_vec()),
2495        );
2496
2497        let mut init_mgr = LinkManager::new();
2498
2499        // Complete handshake (without identification)
2500        let (link_id, init_actions) = init_mgr.create_link(
2501            &dest_hash,
2502            &sig_pub_bytes,
2503            1,
2504            constants::MTU as u32,
2505            &mut rng,
2506        );
2507        let lr_raw = extract_send_packet(&init_actions);
2508        let lr_pkt = RawPacket::unpack(&lr_raw).unwrap();
2509        let resp_actions = resp_mgr.handle_local_delivery(
2510            lr_pkt.destination_hash,
2511            &lr_raw,
2512            lr_pkt.packet_hash,
2513            rns_core::transport::types::InterfaceId(0),
2514            &mut rng,
2515        );
2516        let lrproof_raw = extract_send_packet_at(&resp_actions, 1);
2517        let lrproof_pkt = RawPacket::unpack(&lrproof_raw).unwrap();
2518        let init_actions2 = init_mgr.handle_local_delivery(
2519            lrproof_pkt.destination_hash,
2520            &lrproof_raw,
2521            lrproof_pkt.packet_hash,
2522            rns_core::transport::types::InterfaceId(0),
2523            &mut rng,
2524        );
2525        let lrrtt_raw = extract_any_send_packet(&init_actions2);
2526        let lrrtt_pkt = RawPacket::unpack(&lrrtt_raw).unwrap();
2527        resp_mgr.handle_local_delivery(
2528            lrrtt_pkt.destination_hash,
2529            &lrrtt_raw,
2530            lrrtt_pkt.packet_hash,
2531            rns_core::transport::types::InterfaceId(0),
2532            &mut rng,
2533        );
2534
2535        // Send request without identifying first
2536        let req_actions = init_mgr.send_request(&link_id, "/restricted", b"query", &mut rng);
2537        let req_raw = extract_send_packet_from(&req_actions);
2538        let req_pkt = RawPacket::unpack(&req_raw).unwrap();
2539        let resp_actions = resp_mgr.handle_local_delivery(
2540            req_pkt.destination_hash,
2541            &req_raw,
2542            req_pkt.packet_hash,
2543            rns_core::transport::types::InterfaceId(0),
2544            &mut rng,
2545        );
2546
2547        // Should be denied — no response packet
2548        let has_response = resp_actions
2549            .iter()
2550            .any(|a| matches!(a, LinkManagerAction::SendPacket { .. }));
2551        assert!(!has_response, "Unidentified peer should be denied");
2552    }
2553
2554    #[test]
2555    fn test_teardown_link() {
2556        let mut rng = OsRng;
2557        let dest_hash = [0xDD; 16];
2558        let mut mgr = LinkManager::new();
2559
2560        let dummy_sig = [0xAA; 32];
2561        let (link_id, _) =
2562            mgr.create_link(&dest_hash, &dummy_sig, 1, constants::MTU as u32, &mut rng);
2563        assert_eq!(mgr.link_count(), 1);
2564
2565        let actions = mgr.teardown_link(&link_id);
2566        let has_close = actions
2567            .iter()
2568            .any(|a| matches!(a, LinkManagerAction::LinkClosed { .. }));
2569        assert!(has_close);
2570
2571        // After tick, closed links should be cleaned up
2572        let tick_actions = mgr.tick(&mut rng);
2573        let has_deregister = tick_actions
2574            .iter()
2575            .any(|a| matches!(a, LinkManagerAction::DeregisterLinkDest { .. }));
2576        assert!(has_deregister);
2577        assert_eq!(mgr.link_count(), 0);
2578    }
2579
2580    #[test]
2581    fn test_identify_on_link() {
2582        let mut rng = OsRng;
2583        let dest_hash = [0xDD; 16];
2584        let mut resp_mgr = LinkManager::new();
2585        let (sig_prv, sig_pub_bytes) = make_dest_keys(&mut rng);
2586        resp_mgr.register_link_destination(
2587            dest_hash,
2588            sig_prv,
2589            sig_pub_bytes,
2590            ResourceStrategy::AcceptNone,
2591        );
2592        let mut init_mgr = LinkManager::new();
2593
2594        // Complete handshake
2595        let (link_id, init_actions) = init_mgr.create_link(
2596            &dest_hash,
2597            &sig_pub_bytes,
2598            1,
2599            constants::MTU as u32,
2600            &mut rng,
2601        );
2602        let lr_raw = extract_send_packet(&init_actions);
2603        let lr_pkt = RawPacket::unpack(&lr_raw).unwrap();
2604        let resp_actions = resp_mgr.handle_local_delivery(
2605            lr_pkt.destination_hash,
2606            &lr_raw,
2607            lr_pkt.packet_hash,
2608            rns_core::transport::types::InterfaceId(0),
2609            &mut rng,
2610        );
2611        let lrproof_raw = extract_send_packet_at(&resp_actions, 1);
2612        let lrproof_pkt = RawPacket::unpack(&lrproof_raw).unwrap();
2613        let init_actions2 = init_mgr.handle_local_delivery(
2614            lrproof_pkt.destination_hash,
2615            &lrproof_raw,
2616            lrproof_pkt.packet_hash,
2617            rns_core::transport::types::InterfaceId(0),
2618            &mut rng,
2619        );
2620        let lrrtt_raw = extract_any_send_packet(&init_actions2);
2621        let lrrtt_pkt = RawPacket::unpack(&lrrtt_raw).unwrap();
2622        resp_mgr.handle_local_delivery(
2623            lrrtt_pkt.destination_hash,
2624            &lrrtt_raw,
2625            lrrtt_pkt.packet_hash,
2626            rns_core::transport::types::InterfaceId(0),
2627            &mut rng,
2628        );
2629
2630        // Identify initiator to responder
2631        let identity = Identity::new(&mut rng);
2632        let id_actions = init_mgr.identify(&link_id, &identity, &mut rng);
2633        assert_eq!(id_actions.len(), 1);
2634
2635        // Deliver identify to responder
2636        let id_raw = extract_send_packet_from(&id_actions);
2637        let id_pkt = RawPacket::unpack(&id_raw).unwrap();
2638        let resp_actions = resp_mgr.handle_local_delivery(
2639            id_pkt.destination_hash,
2640            &id_raw,
2641            id_pkt.packet_hash,
2642            rns_core::transport::types::InterfaceId(0),
2643            &mut rng,
2644        );
2645
2646        let has_identified = resp_actions
2647            .iter()
2648            .any(|a| matches!(a, LinkManagerAction::RemoteIdentified { .. }));
2649        assert!(has_identified, "Responder should emit RemoteIdentified");
2650    }
2651
2652    #[test]
2653    fn test_path_hash_computation() {
2654        let h1 = compute_path_hash("/status");
2655        let h2 = compute_path_hash("/path");
2656        assert_ne!(h1, h2);
2657
2658        // Deterministic
2659        assert_eq!(h1, compute_path_hash("/status"));
2660    }
2661
2662    #[test]
2663    fn test_link_count() {
2664        let mut mgr = LinkManager::new();
2665        let mut rng = OsRng;
2666
2667        assert_eq!(mgr.link_count(), 0);
2668
2669        let dummy_sig = [0xAA; 32];
2670        mgr.create_link(&[0x11; 16], &dummy_sig, 1, constants::MTU as u32, &mut rng);
2671        assert_eq!(mgr.link_count(), 1);
2672
2673        mgr.create_link(&[0x22; 16], &dummy_sig, 1, constants::MTU as u32, &mut rng);
2674        assert_eq!(mgr.link_count(), 2);
2675    }
2676
2677    // --- Test helpers ---
2678
2679    fn extract_send_packet(actions: &[LinkManagerAction]) -> Vec<u8> {
2680        extract_send_packet_at(actions, actions.len() - 1)
2681    }
2682
2683    fn extract_send_packet_at(actions: &[LinkManagerAction], idx: usize) -> Vec<u8> {
2684        match &actions[idx] {
2685            LinkManagerAction::SendPacket { raw, .. } => raw.clone(),
2686            other => panic!("Expected SendPacket at index {}, got {:?}", idx, other),
2687        }
2688    }
2689
2690    fn extract_any_send_packet(actions: &[LinkManagerAction]) -> Vec<u8> {
2691        actions
2692            .iter()
2693            .find_map(|a| match a {
2694                LinkManagerAction::SendPacket { raw, .. } => Some(raw.clone()),
2695                _ => None,
2696            })
2697            .expect("Expected at least one SendPacket action")
2698    }
2699
2700    fn extract_send_packet_from(actions: &[LinkManagerAction]) -> Vec<u8> {
2701        extract_any_send_packet(actions)
2702    }
2703
2704    /// Set up two linked managers with an active link.
2705    /// Returns (initiator_mgr, responder_mgr, link_id).
2706    fn setup_active_link() -> (LinkManager, LinkManager, LinkId) {
2707        let mut rng = OsRng;
2708        let dest_hash = [0xDD; 16];
2709        let mut resp_mgr = LinkManager::new();
2710        let (sig_prv, sig_pub_bytes) = make_dest_keys(&mut rng);
2711        resp_mgr.register_link_destination(
2712            dest_hash,
2713            sig_prv,
2714            sig_pub_bytes,
2715            ResourceStrategy::AcceptNone,
2716        );
2717        let mut init_mgr = LinkManager::new();
2718
2719        let (link_id, init_actions) = init_mgr.create_link(
2720            &dest_hash,
2721            &sig_pub_bytes,
2722            1,
2723            constants::MTU as u32,
2724            &mut rng,
2725        );
2726        let lr_raw = extract_send_packet(&init_actions);
2727        let lr_pkt = RawPacket::unpack(&lr_raw).unwrap();
2728        let resp_actions = resp_mgr.handle_local_delivery(
2729            lr_pkt.destination_hash,
2730            &lr_raw,
2731            lr_pkt.packet_hash,
2732            rns_core::transport::types::InterfaceId(0),
2733            &mut rng,
2734        );
2735        let lrproof_raw = extract_send_packet_at(&resp_actions, 1);
2736        let lrproof_pkt = RawPacket::unpack(&lrproof_raw).unwrap();
2737        let init_actions2 = init_mgr.handle_local_delivery(
2738            lrproof_pkt.destination_hash,
2739            &lrproof_raw,
2740            lrproof_pkt.packet_hash,
2741            rns_core::transport::types::InterfaceId(0),
2742            &mut rng,
2743        );
2744        let lrrtt_raw = extract_any_send_packet(&init_actions2);
2745        let lrrtt_pkt = RawPacket::unpack(&lrrtt_raw).unwrap();
2746        resp_mgr.handle_local_delivery(
2747            lrrtt_pkt.destination_hash,
2748            &lrrtt_raw,
2749            lrrtt_pkt.packet_hash,
2750            rns_core::transport::types::InterfaceId(0),
2751            &mut rng,
2752        );
2753
2754        assert_eq!(init_mgr.link_state(&link_id), Some(LinkState::Active));
2755        assert_eq!(resp_mgr.link_state(&link_id), Some(LinkState::Active));
2756
2757        (init_mgr, resp_mgr, link_id)
2758    }
2759
2760    // ====================================================================
2761    // Phase 8a: Resource wiring tests
2762    // ====================================================================
2763
2764    #[test]
2765    fn test_resource_strategy_default() {
2766        let mut mgr = LinkManager::new();
2767        let mut rng = OsRng;
2768        let dummy_sig = [0xAA; 32];
2769        let (link_id, _) =
2770            mgr.create_link(&[0x11; 16], &dummy_sig, 1, constants::MTU as u32, &mut rng);
2771
2772        // Default strategy is AcceptNone
2773        let link = mgr.links.get(&link_id).unwrap();
2774        assert_eq!(link.resource_strategy, ResourceStrategy::AcceptNone);
2775    }
2776
2777    #[test]
2778    fn test_set_resource_strategy() {
2779        let mut mgr = LinkManager::new();
2780        let mut rng = OsRng;
2781        let dummy_sig = [0xAA; 32];
2782        let (link_id, _) =
2783            mgr.create_link(&[0x11; 16], &dummy_sig, 1, constants::MTU as u32, &mut rng);
2784
2785        mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptAll);
2786        assert_eq!(
2787            mgr.links.get(&link_id).unwrap().resource_strategy,
2788            ResourceStrategy::AcceptAll
2789        );
2790
2791        mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptApp);
2792        assert_eq!(
2793            mgr.links.get(&link_id).unwrap().resource_strategy,
2794            ResourceStrategy::AcceptApp
2795        );
2796    }
2797
2798    #[test]
2799    fn test_send_resource_on_active_link() {
2800        let (mut init_mgr, _resp_mgr, link_id) = setup_active_link();
2801        let mut rng = OsRng;
2802
2803        // Send resource data
2804        let data = vec![0xAB; 100]; // small enough for a single part
2805        let actions = init_mgr.send_resource(&link_id, &data, None, &mut rng);
2806
2807        // Should produce at least a SendPacket (advertisement)
2808        let has_send = actions
2809            .iter()
2810            .any(|a| matches!(a, LinkManagerAction::SendPacket { .. }));
2811        assert!(
2812            has_send,
2813            "send_resource should emit advertisement SendPacket"
2814        );
2815    }
2816
2817    #[test]
2818    fn test_send_resource_on_inactive_link() {
2819        let mut mgr = LinkManager::new();
2820        let mut rng = OsRng;
2821        let dummy_sig = [0xAA; 32];
2822        let (link_id, _) =
2823            mgr.create_link(&[0x11; 16], &dummy_sig, 1, constants::MTU as u32, &mut rng);
2824
2825        // Link is Pending, not Active
2826        let actions = mgr.send_resource(&link_id, b"data", None, &mut rng);
2827        assert!(actions.is_empty(), "Cannot send resource on inactive link");
2828    }
2829
2830    #[test]
2831    fn test_resource_adv_rejected_by_accept_none() {
2832        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
2833        let mut rng = OsRng;
2834
2835        // Responder uses default AcceptNone strategy
2836        // Send resource from initiator
2837        let data = vec![0xCD; 100];
2838        let adv_actions = init_mgr.send_resource(&link_id, &data, None, &mut rng);
2839
2840        // Deliver advertisement to responder
2841        for action in &adv_actions {
2842            if let LinkManagerAction::SendPacket { raw, .. } = action {
2843                let pkt = RawPacket::unpack(raw).unwrap();
2844                let resp_actions = resp_mgr.handle_local_delivery(
2845                    pkt.destination_hash,
2846                    raw,
2847                    pkt.packet_hash,
2848                    rns_core::transport::types::InterfaceId(0),
2849                    &mut rng,
2850                );
2851                // AcceptNone: should not produce ResourceReceived, may produce SendPacket (RCL)
2852                let has_resource_received = resp_actions
2853                    .iter()
2854                    .any(|a| matches!(a, LinkManagerAction::ResourceReceived { .. }));
2855                assert!(
2856                    !has_resource_received,
2857                    "AcceptNone should not accept resource"
2858                );
2859            }
2860        }
2861    }
2862
2863    #[test]
2864    fn test_resource_adv_accepted_by_accept_all() {
2865        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
2866        let mut rng = OsRng;
2867
2868        // Set responder to AcceptAll
2869        resp_mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptAll);
2870
2871        // Send resource from initiator
2872        let data = vec![0xCD; 100];
2873        let adv_actions = init_mgr.send_resource(&link_id, &data, None, &mut rng);
2874
2875        // Deliver advertisement to responder
2876        for action in &adv_actions {
2877            if let LinkManagerAction::SendPacket { raw, .. } = action {
2878                let pkt = RawPacket::unpack(raw).unwrap();
2879                let resp_actions = resp_mgr.handle_local_delivery(
2880                    pkt.destination_hash,
2881                    raw,
2882                    pkt.packet_hash,
2883                    rns_core::transport::types::InterfaceId(0),
2884                    &mut rng,
2885                );
2886                // AcceptAll: should accept and produce a SendPacket (request for parts)
2887                let has_send = resp_actions
2888                    .iter()
2889                    .any(|a| matches!(a, LinkManagerAction::SendPacket { .. }));
2890                assert!(has_send, "AcceptAll should accept and request parts");
2891            }
2892        }
2893    }
2894
2895    #[test]
2896    fn test_resource_accept_app_query() {
2897        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
2898        let mut rng = OsRng;
2899
2900        // Set responder to AcceptApp
2901        resp_mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptApp);
2902
2903        // Send resource from initiator
2904        let data = vec![0xCD; 100];
2905        let adv_actions = init_mgr.send_resource(&link_id, &data, None, &mut rng);
2906
2907        // Deliver advertisement to responder
2908        let mut got_query = false;
2909        for action in &adv_actions {
2910            if let LinkManagerAction::SendPacket { raw, .. } = action {
2911                let pkt = RawPacket::unpack(raw).unwrap();
2912                let resp_actions = resp_mgr.handle_local_delivery(
2913                    pkt.destination_hash,
2914                    raw,
2915                    pkt.packet_hash,
2916                    rns_core::transport::types::InterfaceId(0),
2917                    &mut rng,
2918                );
2919                for a in &resp_actions {
2920                    if matches!(a, LinkManagerAction::ResourceAcceptQuery { .. }) {
2921                        got_query = true;
2922                    }
2923                }
2924            }
2925        }
2926        assert!(got_query, "AcceptApp should emit ResourceAcceptQuery");
2927    }
2928
2929    #[test]
2930    fn test_resource_accept_app_accept() {
2931        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
2932        let mut rng = OsRng;
2933
2934        resp_mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptApp);
2935
2936        let data = vec![0xCD; 100];
2937        let adv_actions = init_mgr.send_resource(&link_id, &data, None, &mut rng);
2938
2939        for action in &adv_actions {
2940            if let LinkManagerAction::SendPacket { raw, .. } = action {
2941                let pkt = RawPacket::unpack(raw).unwrap();
2942                let resp_actions = resp_mgr.handle_local_delivery(
2943                    pkt.destination_hash,
2944                    raw,
2945                    pkt.packet_hash,
2946                    rns_core::transport::types::InterfaceId(0),
2947                    &mut rng,
2948                );
2949                for a in &resp_actions {
2950                    if let LinkManagerAction::ResourceAcceptQuery {
2951                        link_id: lid,
2952                        resource_hash,
2953                        ..
2954                    } = a
2955                    {
2956                        // Accept the resource
2957                        let accept_actions =
2958                            resp_mgr.accept_resource(lid, resource_hash, true, &mut rng);
2959                        // Should produce a SendPacket (request for parts)
2960                        let has_send = accept_actions
2961                            .iter()
2962                            .any(|a| matches!(a, LinkManagerAction::SendPacket { .. }));
2963                        assert!(
2964                            has_send,
2965                            "Accepting resource should produce request for parts"
2966                        );
2967                    }
2968                }
2969            }
2970        }
2971    }
2972
2973    #[test]
2974    fn test_resource_accept_app_reject() {
2975        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
2976        let mut rng = OsRng;
2977
2978        resp_mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptApp);
2979
2980        let data = vec![0xCD; 100];
2981        let adv_actions = init_mgr.send_resource(&link_id, &data, None, &mut rng);
2982
2983        for action in &adv_actions {
2984            if let LinkManagerAction::SendPacket { raw, .. } = action {
2985                let pkt = RawPacket::unpack(raw).unwrap();
2986                let resp_actions = resp_mgr.handle_local_delivery(
2987                    pkt.destination_hash,
2988                    raw,
2989                    pkt.packet_hash,
2990                    rns_core::transport::types::InterfaceId(0),
2991                    &mut rng,
2992                );
2993                for a in &resp_actions {
2994                    if let LinkManagerAction::ResourceAcceptQuery {
2995                        link_id: lid,
2996                        resource_hash,
2997                        ..
2998                    } = a
2999                    {
3000                        // Reject the resource
3001                        let reject_actions =
3002                            resp_mgr.accept_resource(lid, resource_hash, false, &mut rng);
3003                        // Rejecting should send a cancel and not request parts
3004                        // No ResourceReceived should appear
3005                        let has_resource_received = reject_actions
3006                            .iter()
3007                            .any(|a| matches!(a, LinkManagerAction::ResourceReceived { .. }));
3008                        assert!(!has_resource_received);
3009                    }
3010                }
3011            }
3012        }
3013    }
3014
3015    #[test]
3016    fn test_resource_full_transfer() {
3017        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
3018        let mut rng = OsRng;
3019
3020        // Set responder to AcceptAll
3021        resp_mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptAll);
3022
3023        // Small data (fits in single SDU)
3024        let original_data = b"Hello, Resource Transfer!".to_vec();
3025        let adv_actions = init_mgr.send_resource(&link_id, &original_data, None, &mut rng);
3026
3027        // Drive the full transfer protocol between the two managers.
3028        // Tag each SendPacket with its source ('i' = initiator, 'r' = responder).
3029        let mut pending: Vec<(char, LinkManagerAction)> =
3030            adv_actions.into_iter().map(|a| ('i', a)).collect();
3031        let mut rounds = 0;
3032        let max_rounds = 50;
3033        let mut resource_received = false;
3034        let mut sender_completed = false;
3035
3036        while !pending.is_empty() && rounds < max_rounds {
3037            rounds += 1;
3038            let mut next: Vec<(char, LinkManagerAction)> = Vec::new();
3039
3040            for (source, action) in pending.drain(..) {
3041                if let LinkManagerAction::SendPacket { raw, .. } = action {
3042                    let pkt = RawPacket::unpack(&raw).unwrap();
3043
3044                    // Deliver only to the OTHER side
3045                    let target_actions = if source == 'i' {
3046                        resp_mgr.handle_local_delivery(
3047                            pkt.destination_hash,
3048                            &raw,
3049                            pkt.packet_hash,
3050                            rns_core::transport::types::InterfaceId(0),
3051                            &mut rng,
3052                        )
3053                    } else {
3054                        init_mgr.handle_local_delivery(
3055                            pkt.destination_hash,
3056                            &raw,
3057                            pkt.packet_hash,
3058                            rns_core::transport::types::InterfaceId(0),
3059                            &mut rng,
3060                        )
3061                    };
3062
3063                    let target_source = if source == 'i' { 'r' } else { 'i' };
3064                    for a in &target_actions {
3065                        match a {
3066                            LinkManagerAction::ResourceReceived { data, .. } => {
3067                                assert_eq!(*data, original_data);
3068                                resource_received = true;
3069                            }
3070                            LinkManagerAction::ResourceCompleted { .. } => {
3071                                sender_completed = true;
3072                            }
3073                            _ => {}
3074                        }
3075                    }
3076                    next.extend(target_actions.into_iter().map(|a| (target_source, a)));
3077                }
3078            }
3079            pending = next;
3080        }
3081
3082        assert!(
3083            resource_received,
3084            "Responder should receive resource data (rounds={})",
3085            rounds
3086        );
3087        assert!(
3088            sender_completed,
3089            "Sender should get completion proof (rounds={})",
3090            rounds
3091        );
3092    }
3093
3094    #[test]
3095    fn test_resource_cancel_icl() {
3096        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
3097        let mut rng = OsRng;
3098
3099        resp_mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptAll);
3100
3101        // Use large data so transfer is multi-part
3102        let data = vec![0xAB; 2000];
3103        let adv_actions = init_mgr.send_resource(&link_id, &data, None, &mut rng);
3104
3105        // Deliver advertisement — responder accepts and sends request
3106        for action in &adv_actions {
3107            if let LinkManagerAction::SendPacket { raw, .. } = action {
3108                let pkt = RawPacket::unpack(raw).unwrap();
3109                resp_mgr.handle_local_delivery(
3110                    pkt.destination_hash,
3111                    raw,
3112                    pkt.packet_hash,
3113                    rns_core::transport::types::InterfaceId(0),
3114                    &mut rng,
3115                );
3116            }
3117        }
3118
3119        // Verify there are incoming resources on the responder
3120        assert!(!resp_mgr
3121            .links
3122            .get(&link_id)
3123            .unwrap()
3124            .incoming_resources
3125            .is_empty());
3126
3127        // Simulate ICL (cancel from initiator side) by calling handle_resource_icl
3128        let icl_actions = resp_mgr.handle_resource_icl(&link_id);
3129
3130        // Should have resource failed
3131        let has_failed = icl_actions
3132            .iter()
3133            .any(|a| matches!(a, LinkManagerAction::ResourceFailed { .. }));
3134        assert!(has_failed, "ICL should produce ResourceFailed");
3135    }
3136
3137    #[test]
3138    fn test_resource_cancel_rcl() {
3139        let (mut init_mgr, _resp_mgr, link_id) = setup_active_link();
3140        let mut rng = OsRng;
3141
3142        // Create a resource sender
3143        let data = vec![0xAB; 2000];
3144        init_mgr.send_resource(&link_id, &data, None, &mut rng);
3145
3146        // Verify there are outgoing resources
3147        assert!(!init_mgr
3148            .links
3149            .get(&link_id)
3150            .unwrap()
3151            .outgoing_resources
3152            .is_empty());
3153
3154        // Simulate RCL (cancel from receiver side)
3155        let rcl_actions = init_mgr.handle_resource_rcl(&link_id);
3156
3157        let has_failed = rcl_actions
3158            .iter()
3159            .any(|a| matches!(a, LinkManagerAction::ResourceFailed { .. }));
3160        assert!(has_failed, "RCL should produce ResourceFailed");
3161    }
3162
3163    #[test]
3164    fn test_resource_tick_cleans_up() {
3165        let (mut init_mgr, _resp_mgr, link_id) = setup_active_link();
3166        let mut rng = OsRng;
3167
3168        let data = vec![0xAB; 100];
3169        init_mgr.send_resource(&link_id, &data, None, &mut rng);
3170
3171        assert!(!init_mgr
3172            .links
3173            .get(&link_id)
3174            .unwrap()
3175            .outgoing_resources
3176            .is_empty());
3177
3178        // Cancel the sender to make it Complete
3179        init_mgr.handle_resource_rcl(&link_id);
3180
3181        // Tick should clean up completed resources
3182        init_mgr.tick(&mut rng);
3183
3184        assert!(
3185            init_mgr
3186                .links
3187                .get(&link_id)
3188                .unwrap()
3189                .outgoing_resources
3190                .is_empty(),
3191            "Tick should clean up completed/failed outgoing resources"
3192        );
3193    }
3194
3195    #[test]
3196    fn test_build_link_packet() {
3197        let (init_mgr, _resp_mgr, link_id) = setup_active_link();
3198
3199        let actions =
3200            init_mgr.build_link_packet(&link_id, constants::CONTEXT_RESOURCE, b"test data");
3201        assert_eq!(actions.len(), 1);
3202        if let LinkManagerAction::SendPacket { raw, dest_type, .. } = &actions[0] {
3203            let pkt = RawPacket::unpack(raw).unwrap();
3204            assert_eq!(pkt.context, constants::CONTEXT_RESOURCE);
3205            assert_eq!(*dest_type, constants::DESTINATION_LINK);
3206        } else {
3207            panic!("Expected SendPacket");
3208        }
3209    }
3210
3211    // ====================================================================
3212    // Phase 8b: Channel message & data callback tests
3213    // ====================================================================
3214
3215    #[test]
3216    fn test_channel_message_delivery() {
3217        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
3218        let mut rng = OsRng;
3219
3220        // Send channel message from initiator
3221        let chan_actions = init_mgr.send_channel_message(&link_id, 42, b"channel data", &mut rng);
3222        assert!(!chan_actions.is_empty());
3223
3224        // Deliver to responder
3225        let mut got_channel_msg = false;
3226        for action in &chan_actions {
3227            if let LinkManagerAction::SendPacket { raw, .. } = action {
3228                let pkt = RawPacket::unpack(raw).unwrap();
3229                let resp_actions = resp_mgr.handle_local_delivery(
3230                    pkt.destination_hash,
3231                    raw,
3232                    pkt.packet_hash,
3233                    rns_core::transport::types::InterfaceId(0),
3234                    &mut rng,
3235                );
3236                for a in &resp_actions {
3237                    if let LinkManagerAction::ChannelMessageReceived {
3238                        msgtype, payload, ..
3239                    } = a
3240                    {
3241                        assert_eq!(*msgtype, 42);
3242                        assert_eq!(*payload, b"channel data");
3243                        got_channel_msg = true;
3244                    }
3245                }
3246            }
3247        }
3248        assert!(got_channel_msg, "Responder should receive channel message");
3249    }
3250
3251    #[test]
3252    fn test_generic_link_data_delivery() {
3253        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
3254        let mut rng = OsRng;
3255
3256        // Send generic data with a custom context
3257        let actions = init_mgr.send_on_link(&link_id, b"raw stuff", 0x42, &mut rng);
3258        assert_eq!(actions.len(), 1);
3259
3260        // Deliver to responder
3261        let raw = extract_any_send_packet(&actions);
3262        let pkt = RawPacket::unpack(&raw).unwrap();
3263        let resp_actions = resp_mgr.handle_local_delivery(
3264            pkt.destination_hash,
3265            &raw,
3266            pkt.packet_hash,
3267            rns_core::transport::types::InterfaceId(0),
3268            &mut rng,
3269        );
3270
3271        let has_data = resp_actions
3272            .iter()
3273            .any(|a| matches!(a, LinkManagerAction::LinkDataReceived { context: 0x42, .. }));
3274        assert!(
3275            has_data,
3276            "Responder should receive LinkDataReceived for unknown context"
3277        );
3278    }
3279
3280    #[test]
3281    fn test_response_delivery() {
3282        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
3283        let mut rng = OsRng;
3284
3285        // Register handler on responder
3286        resp_mgr.register_request_handler("/echo", None, |_link_id, _path, data, _remote| {
3287            Some(data.to_vec())
3288        });
3289
3290        // Send request from initiator
3291        let req_actions = init_mgr.send_request(&link_id, "/echo", b"\xc0", &mut rng); // msgpack nil
3292        assert!(!req_actions.is_empty());
3293
3294        // Deliver request to responder — should produce response
3295        let req_raw = extract_any_send_packet(&req_actions);
3296        let req_pkt = RawPacket::unpack(&req_raw).unwrap();
3297        let resp_actions = resp_mgr.handle_local_delivery(
3298            req_pkt.destination_hash,
3299            &req_raw,
3300            req_pkt.packet_hash,
3301            rns_core::transport::types::InterfaceId(0),
3302            &mut rng,
3303        );
3304        let has_resp_send = resp_actions
3305            .iter()
3306            .any(|a| matches!(a, LinkManagerAction::SendPacket { .. }));
3307        assert!(has_resp_send, "Handler should produce response");
3308
3309        // Deliver response back to initiator
3310        let resp_raw = extract_any_send_packet(&resp_actions);
3311        let resp_pkt = RawPacket::unpack(&resp_raw).unwrap();
3312        let init_actions = init_mgr.handle_local_delivery(
3313            resp_pkt.destination_hash,
3314            &resp_raw,
3315            resp_pkt.packet_hash,
3316            rns_core::transport::types::InterfaceId(0),
3317            &mut rng,
3318        );
3319
3320        let has_response_received = init_actions
3321            .iter()
3322            .any(|a| matches!(a, LinkManagerAction::ResponseReceived { .. }));
3323        assert!(
3324            has_response_received,
3325            "Initiator should receive ResponseReceived"
3326        );
3327    }
3328
3329    #[test]
3330    fn test_send_channel_message_on_no_channel() {
3331        let mut mgr = LinkManager::new();
3332        let mut rng = OsRng;
3333        let dummy_sig = [0xAA; 32];
3334        let (link_id, _) =
3335            mgr.create_link(&[0x11; 16], &dummy_sig, 1, constants::MTU as u32, &mut rng);
3336
3337        // Link is Pending (no channel), should return empty
3338        let actions = mgr.send_channel_message(&link_id, 1, b"test", &mut rng);
3339        assert!(actions.is_empty(), "No channel on pending link");
3340    }
3341
3342    #[test]
3343    fn test_send_on_link_requires_active() {
3344        let mut mgr = LinkManager::new();
3345        let mut rng = OsRng;
3346        let dummy_sig = [0xAA; 32];
3347        let (link_id, _) =
3348            mgr.create_link(&[0x11; 16], &dummy_sig, 1, constants::MTU as u32, &mut rng);
3349
3350        let actions = mgr.send_on_link(&link_id, b"test", constants::CONTEXT_NONE, &mut rng);
3351        assert!(actions.is_empty(), "Cannot send on pending link");
3352    }
3353
3354    #[test]
3355    fn test_send_on_link_unknown_link() {
3356        let mgr = LinkManager::new();
3357        let mut rng = OsRng;
3358
3359        let actions = mgr.send_on_link(&[0xFF; 16], b"test", constants::CONTEXT_NONE, &mut rng);
3360        assert!(actions.is_empty());
3361    }
3362
3363    #[test]
3364    fn test_resource_full_transfer_large() {
3365        let (mut init_mgr, mut resp_mgr, link_id) = setup_active_link();
3366        let mut rng = OsRng;
3367
3368        resp_mgr.set_resource_strategy(&link_id, ResourceStrategy::AcceptAll);
3369
3370        // Multi-part data (larger than a single SDU of 464 bytes)
3371        let original_data: Vec<u8> = (0..2000u32)
3372            .map(|i| {
3373                let pos = i as usize;
3374                (pos ^ (pos >> 8) ^ (pos >> 16)) as u8
3375            })
3376            .collect();
3377
3378        let adv_actions = init_mgr.send_resource(&link_id, &original_data, None, &mut rng);
3379
3380        let mut pending: Vec<(char, LinkManagerAction)> =
3381            adv_actions.into_iter().map(|a| ('i', a)).collect();
3382        let mut rounds = 0;
3383        let max_rounds = 200;
3384        let mut resource_received = false;
3385        let mut sender_completed = false;
3386
3387        while !pending.is_empty() && rounds < max_rounds {
3388            rounds += 1;
3389            let mut next: Vec<(char, LinkManagerAction)> = Vec::new();
3390
3391            for (source, action) in pending.drain(..) {
3392                if let LinkManagerAction::SendPacket { raw, .. } = action {
3393                    let pkt = match RawPacket::unpack(&raw) {
3394                        Ok(p) => p,
3395                        Err(_) => continue,
3396                    };
3397
3398                    let target_actions = if source == 'i' {
3399                        resp_mgr.handle_local_delivery(
3400                            pkt.destination_hash,
3401                            &raw,
3402                            pkt.packet_hash,
3403                            rns_core::transport::types::InterfaceId(0),
3404                            &mut rng,
3405                        )
3406                    } else {
3407                        init_mgr.handle_local_delivery(
3408                            pkt.destination_hash,
3409                            &raw,
3410                            pkt.packet_hash,
3411                            rns_core::transport::types::InterfaceId(0),
3412                            &mut rng,
3413                        )
3414                    };
3415
3416                    let target_source = if source == 'i' { 'r' } else { 'i' };
3417                    for a in &target_actions {
3418                        match a {
3419                            LinkManagerAction::ResourceReceived { data, .. } => {
3420                                assert_eq!(*data, original_data);
3421                                resource_received = true;
3422                            }
3423                            LinkManagerAction::ResourceCompleted { .. } => {
3424                                sender_completed = true;
3425                            }
3426                            _ => {}
3427                        }
3428                    }
3429                    next.extend(target_actions.into_iter().map(|a| (target_source, a)));
3430                }
3431            }
3432            pending = next;
3433        }
3434
3435        assert!(
3436            resource_received,
3437            "Should receive large resource (rounds={})",
3438            rounds
3439        );
3440        assert!(
3441            sender_completed,
3442            "Sender should complete (rounds={})",
3443            rounds
3444        );
3445    }
3446
3447    #[test]
3448    fn test_process_resource_actions_mapping() {
3449        let (init_mgr, _resp_mgr, link_id) = setup_active_link();
3450        let mut rng = OsRng;
3451
3452        // Test that various ResourceActions map to correct LinkManagerActions
3453        let actions = vec![
3454            ResourceAction::DataReceived {
3455                data: vec![1, 2, 3],
3456                metadata: Some(vec![4, 5]),
3457            },
3458            ResourceAction::Completed,
3459            ResourceAction::Failed(rns_core::resource::ResourceError::Timeout),
3460            ResourceAction::ProgressUpdate {
3461                received: 10,
3462                total: 20,
3463            },
3464        ];
3465
3466        let result = init_mgr.process_resource_actions(&link_id, actions, &mut rng);
3467
3468        assert!(matches!(
3469            result[0],
3470            LinkManagerAction::ResourceReceived { .. }
3471        ));
3472        assert!(matches!(
3473            result[1],
3474            LinkManagerAction::ResourceCompleted { .. }
3475        ));
3476        assert!(matches!(
3477            result[2],
3478            LinkManagerAction::ResourceFailed { .. }
3479        ));
3480        assert!(matches!(
3481            result[3],
3482            LinkManagerAction::ResourceProgress {
3483                received: 10,
3484                total: 20,
3485                ..
3486            }
3487        ));
3488    }
3489
3490    #[test]
3491    fn test_link_state_empty() {
3492        let mgr = LinkManager::new();
3493        let fake_id = [0xAA; 16];
3494        assert!(mgr.link_state(&fake_id).is_none());
3495    }
3496}