Skip to main content

rns_core/link/
mod.rs

1pub mod crypto;
2pub mod handshake;
3pub mod identify;
4pub mod keepalive;
5pub mod types;
6
7use alloc::vec::Vec;
8
9use rns_crypto::ed25519::{Ed25519PrivateKey, Ed25519PublicKey};
10use rns_crypto::token::Token;
11use rns_crypto::x25519::X25519PrivateKey;
12use rns_crypto::Rng;
13
14use crate::constants::{
15    LINK_ECPUBSIZE, LINK_ESTABLISHMENT_TIMEOUT_PER_HOP, LINK_KEEPALIVE_MAX, MTU, PATHFINDER_M,
16};
17
18pub use types::{LinkAction, LinkError, LinkId, LinkMode, LinkState, TeardownReason};
19
20use crypto::{create_session_token, link_decrypt, link_encrypt};
21use handshake::{
22    build_linkrequest_data, compute_link_id, pack_rtt, parse_linkrequest_data,
23    perform_key_exchange, unpack_rtt, validate_lrproof,
24};
25use keepalive::{
26    compute_establishment_timeout, compute_keepalive, compute_stale_time, is_establishment_timeout,
27    should_go_stale, should_send_keepalive,
28};
29
30/// The Link Engine manages a single link's lifecycle.
31///
32/// It follows the action-queue model: methods return `Vec<LinkAction>` instead
33/// of performing I/O directly. The caller dispatches actions.
34pub struct LinkEngine {
35    link_id: LinkId,
36    state: LinkState,
37    is_initiator: bool,
38    mode: LinkMode,
39
40    // Ephemeral keys
41    prv: X25519PrivateKey,
42
43    // Peer keys
44    peer_pub_bytes: Option<[u8; 32]>,
45    peer_sig_pub_bytes: Option<[u8; 32]>,
46
47    // Session crypto
48    derived_key: Option<Vec<u8>>,
49    token: Option<Token>,
50
51    // Timing
52    request_time: f64,
53    activated_at: Option<f64>,
54    last_inbound: f64,
55    last_outbound: f64,
56    last_keepalive: f64,
57    last_proof: f64,
58    rtt: Option<f64>,
59    keepalive_interval: f64,
60    stale_time: f64,
61    establishment_timeout: f64,
62    expected_hops: u8,
63    /// Time of the first authenticated terminus hop rebalance.
64    rebalanced_at: Option<f64>,
65
66    // Counts packed packet data/ciphertext, matching Python Link accounting.
67    tx_packets: u64,
68    rx_packets: u64,
69    tx_bytes: u64,
70    rx_bytes: u64,
71
72    // Identity
73    remote_identity: Option<([u8; 16], [u8; 64])>,
74    destination_hash: [u8; 16],
75
76    // MDU
77    mtu: u32,
78    mdu: usize,
79}
80
81impl LinkEngine {
82    /// Create a new initiator-side link engine.
83    ///
84    /// Returns `(engine, linkrequest_data)` — the caller must pack linkrequest_data
85    /// into a LINKREQUEST packet and send it.
86    pub fn new_initiator(
87        dest_hash: &[u8; 16],
88        hops: u8,
89        mode: LinkMode,
90        mtu: Option<u32>,
91        now: f64,
92        rng: &mut dyn Rng,
93    ) -> (Self, Vec<u8>) {
94        let prv = X25519PrivateKey::generate(rng);
95        let pub_bytes = prv.public_key().public_bytes();
96        let sig_prv = Ed25519PrivateKey::generate(rng);
97        let sig_pub_bytes = sig_prv.public_key().public_bytes();
98
99        let request_data = build_linkrequest_data(&pub_bytes, &sig_pub_bytes, mtu, mode);
100
101        let link_mtu = mtu.unwrap_or(MTU as u32);
102
103        let engine = LinkEngine {
104            link_id: [0u8; 16], // will be set after packet is built
105            state: LinkState::Pending,
106            is_initiator: true,
107            mode,
108            prv,
109            peer_pub_bytes: None,
110            peer_sig_pub_bytes: None,
111            derived_key: None,
112            token: None,
113            request_time: now,
114            activated_at: None,
115            last_inbound: now,
116            last_outbound: now,
117            last_keepalive: now,
118            last_proof: 0.0,
119            rtt: None,
120            keepalive_interval: LINK_KEEPALIVE_MAX,
121            stale_time: LINK_KEEPALIVE_MAX * 2.0,
122            establishment_timeout: compute_establishment_timeout(
123                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
124                hops,
125            ),
126            expected_hops: if hops < PATHFINDER_M {
127                hops
128            } else {
129                PATHFINDER_M
130            },
131            rebalanced_at: None,
132            tx_packets: 0,
133            rx_packets: 0,
134            tx_bytes: 0,
135            rx_bytes: 0,
136            remote_identity: None,
137            destination_hash: *dest_hash,
138            mtu: link_mtu,
139            mdu: compute_mdu(link_mtu as usize),
140        };
141
142        (engine, request_data)
143    }
144
145    /// Set link_id from the hashable part of the packed LINKREQUEST packet.
146    ///
147    /// Must be called after packing the LINKREQUEST packet (since link_id depends
148    /// on the packet's hashable part).
149    pub fn set_link_id_from_hashable(&mut self, hashable_part: &[u8], data_len: usize) {
150        let extra = data_len.saturating_sub(LINK_ECPUBSIZE);
151        self.link_id = compute_link_id(hashable_part, extra);
152    }
153
154    /// Create a new responder-side link engine from an incoming LINKREQUEST.
155    ///
156    /// `owner_sig_prv` / `owner_sig_pub` are the destination's signing keys.
157    /// Returns `(engine, actions)` where actions include the LRPROOF data to send.
158    #[allow(clippy::too_many_arguments)]
159    pub fn new_responder(
160        owner_sig_prv: &Ed25519PrivateKey,
161        owner_sig_pub_bytes: &[u8; 32],
162        linkrequest_data: &[u8],
163        hashable_part: &[u8],
164        dest_hash: &[u8; 16],
165        hops: u8,
166        now: f64,
167        rng: &mut dyn Rng,
168    ) -> Result<(Self, Vec<u8>), LinkError> {
169        let (peer_pub, peer_sig_pub, peer_mtu, mode) = parse_linkrequest_data(linkrequest_data)?;
170
171        let extra = linkrequest_data.len().saturating_sub(LINK_ECPUBSIZE);
172        let link_id = compute_link_id(hashable_part, extra);
173
174        // Generate ephemeral keys for this end
175        let prv = X25519PrivateKey::generate(rng);
176        let pub_bytes = prv.public_key().public_bytes();
177        let sig_pub_bytes = *owner_sig_pub_bytes;
178
179        // Perform ECDH + HKDF
180        let derived_key = perform_key_exchange(&prv, &peer_pub, &link_id, mode)?;
181        let token = create_session_token(&derived_key)?;
182
183        let link_mtu = peer_mtu.unwrap_or(MTU as u32);
184
185        // Build LRPROOF
186        let lrproof_data = handshake::build_lrproof(
187            &link_id,
188            &pub_bytes,
189            &sig_pub_bytes,
190            owner_sig_prv,
191            peer_mtu,
192            mode,
193        );
194
195        let engine = LinkEngine {
196            link_id,
197            state: LinkState::Handshake,
198            is_initiator: false,
199            mode,
200            prv,
201            peer_pub_bytes: Some(peer_pub),
202            peer_sig_pub_bytes: Some(peer_sig_pub),
203            derived_key: Some(derived_key),
204            token: Some(token),
205            request_time: now,
206            activated_at: None,
207            last_inbound: now,
208            last_outbound: now,
209            last_keepalive: now,
210            last_proof: 0.0,
211            rtt: None,
212            keepalive_interval: LINK_KEEPALIVE_MAX,
213            stale_time: LINK_KEEPALIVE_MAX * 2.0,
214            establishment_timeout: compute_establishment_timeout(
215                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
216                hops,
217            ),
218            expected_hops: PATHFINDER_M,
219            rebalanced_at: None,
220            tx_packets: 0,
221            rx_packets: 0,
222            tx_bytes: 0,
223            rx_bytes: 0,
224            remote_identity: None,
225            destination_hash: *dest_hash,
226            mtu: link_mtu,
227            mdu: compute_mdu(link_mtu as usize),
228        };
229
230        Ok((engine, lrproof_data))
231    }
232
233    /// Handle an incoming LRPROOF (initiator side).
234    ///
235    /// Validates the proof, performs ECDH, derives session key, returns LRRTT data
236    /// to be encrypted and sent.
237    pub fn handle_lrproof(
238        &mut self,
239        proof_data: &[u8],
240        peer_sig_pub_bytes: &[u8; 32],
241        now: f64,
242        rng: &mut dyn Rng,
243    ) -> Result<(Vec<u8>, Vec<LinkAction>), LinkError> {
244        self.handle_lrproof_with_hops(proof_data, peer_sig_pub_bytes, None, now, rng)
245    }
246
247    /// Validate an LRPROOF while recording its authenticated hop metric.
248    pub fn handle_lrproof_with_hops(
249        &mut self,
250        proof_data: &[u8],
251        peer_sig_pub_bytes: &[u8; 32],
252        packet_hops: Option<u8>,
253        now: f64,
254        rng: &mut dyn Rng,
255    ) -> Result<(Vec<u8>, Vec<LinkAction>), LinkError> {
256        if self.state != LinkState::Pending || !self.is_initiator {
257            return Err(LinkError::InvalidState);
258        }
259
260        let peer_sig_pub = Ed25519PublicKey::from_bytes(peer_sig_pub_bytes);
261
262        let (peer_pub, confirmed_mtu, confirmed_mode) =
263            validate_lrproof(proof_data, &self.link_id, &peer_sig_pub, peer_sig_pub_bytes)?;
264
265        if confirmed_mode != self.mode {
266            return Err(LinkError::UnsupportedMode);
267        }
268
269        if let Some(hops) = packet_hops {
270            if hops != self.expected_hops && self.rebalanced_at.is_none() {
271                self.rebalanced_at = Some(now);
272                self.expected_hops = hops;
273            }
274        }
275
276        self.peer_pub_bytes = Some(peer_pub);
277        self.peer_sig_pub_bytes = Some(*peer_sig_pub_bytes);
278
279        // ECDH + HKDF
280        let derived_key = perform_key_exchange(&self.prv, &peer_pub, &self.link_id, self.mode)?;
281        let token = create_session_token(&derived_key)?;
282
283        self.derived_key = Some(derived_key);
284        self.token = Some(token);
285
286        // Update MTU if confirmed
287        if let Some(mtu) = confirmed_mtu {
288            self.mtu = mtu;
289            self.mdu = compute_mdu(mtu as usize);
290        }
291
292        // Compute RTT and activate
293        let rtt = now - self.request_time;
294        self.rtt = Some(rtt);
295        self.state = LinkState::Active;
296        self.activated_at = Some(now);
297        self.last_inbound = now;
298        self.update_keepalive();
299
300        // Build encrypted LRRTT packet data
301        let rtt_packed = pack_rtt(rtt);
302        let rtt_encrypted = self.encrypt(&rtt_packed, rng)?;
303
304        let actions = vec![
305            LinkAction::StateChanged {
306                link_id: self.link_id,
307                new_state: LinkState::Active,
308                reason: None,
309            },
310            LinkAction::LinkEstablished {
311                link_id: self.link_id,
312                rtt,
313                is_initiator: true,
314            },
315        ];
316
317        Ok((rtt_encrypted, actions))
318    }
319
320    /// Handle an incoming LRRTT (responder side).
321    ///
322    /// Decrypts the RTT packet, activates the link.
323    pub fn handle_lrrtt(
324        &mut self,
325        encrypted_data: &[u8],
326        now: f64,
327    ) -> Result<Vec<LinkAction>, LinkError> {
328        self.handle_lrrtt_with_hops(encrypted_data, None, now)
329    }
330
331    /// Handle LRRTT while recording the responder-side hop metric. The legacy
332    /// timing-only method remains available for existing callers.
333    pub fn handle_lrrtt_with_hops(
334        &mut self,
335        encrypted_data: &[u8],
336        packet_hops: Option<u8>,
337        now: f64,
338    ) -> Result<Vec<LinkAction>, LinkError> {
339        if self.state != LinkState::Handshake || self.is_initiator {
340            return Err(LinkError::InvalidState);
341        }
342
343        let plaintext = self.decrypt(encrypted_data)?;
344        let initiator_rtt = unpack_rtt(&plaintext).ok_or(LinkError::InvalidData)?;
345        if let Some(hops) = packet_hops {
346            self.expected_hops = hops;
347        }
348
349        let measured_rtt = now - self.request_time;
350        let rtt = if measured_rtt > initiator_rtt {
351            measured_rtt
352        } else {
353            initiator_rtt
354        };
355
356        self.rtt = Some(rtt);
357        self.state = LinkState::Active;
358        self.activated_at = Some(now);
359        self.last_inbound = now;
360        self.update_keepalive();
361
362        let actions = vec![
363            LinkAction::StateChanged {
364                link_id: self.link_id,
365                new_state: LinkState::Active,
366                reason: None,
367            },
368            LinkAction::LinkEstablished {
369                link_id: self.link_id,
370                rtt,
371                is_initiator: false,
372            },
373        ];
374
375        Ok(actions)
376    }
377
378    /// Encrypt plaintext for transmission over this link.
379    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Result<Vec<u8>, LinkError> {
380        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
381        Ok(link_encrypt(token, plaintext, rng))
382    }
383
384    /// Decrypt ciphertext received on this link.
385    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, LinkError> {
386        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
387        link_decrypt(token, ciphertext)
388    }
389
390    /// Build LINKIDENTIFY data (encrypted).
391    pub fn build_identify(
392        &self,
393        identity: &rns_crypto::identity::Identity,
394        rng: &mut dyn Rng,
395    ) -> Result<Vec<u8>, LinkError> {
396        if self.state != LinkState::Active {
397            return Err(LinkError::InvalidState);
398        }
399        let plaintext = identify::build_identify_data(identity, &self.link_id)?;
400        self.encrypt(&plaintext, rng)
401    }
402
403    /// Handle incoming LINKIDENTIFY (encrypted data).
404    ///
405    /// Only responders (non-initiators) can receive LINKIDENTIFY (Python: Link.py:1017).
406    pub fn handle_identify(&mut self, encrypted_data: &[u8]) -> Result<Vec<LinkAction>, LinkError> {
407        if self.state != LinkState::Active || self.is_initiator {
408            return Err(LinkError::InvalidState);
409        }
410
411        let plaintext = self.decrypt(encrypted_data)?;
412        let (identity_hash, public_key) =
413            identify::validate_identify_data(&plaintext, &self.link_id)?;
414        if self.remote_identity.is_some() {
415            return Ok(Vec::new());
416        }
417        self.remote_identity = Some((identity_hash, public_key));
418
419        Ok(alloc::vec![LinkAction::RemoteIdentified {
420            link_id: self.link_id,
421            identity_hash,
422            public_key,
423        }])
424    }
425
426    /// Record that an inbound packet was received (updates timing).
427    ///
428    /// If the link is STALE, recovers to ACTIVE (Python: Link.py:987-988).
429    pub fn record_inbound(&mut self, now: f64) -> Vec<LinkAction> {
430        self.last_inbound = now;
431        if self.state == LinkState::Stale {
432            self.state = LinkState::Active;
433            return alloc::vec![LinkAction::StateChanged {
434                link_id: self.link_id,
435                new_state: LinkState::Active,
436                reason: None,
437            }];
438        }
439        Vec::new()
440    }
441
442    /// Record that a proof was received (updates timing for stale detection).
443    pub fn record_proof(&mut self, now: f64) {
444        self.last_proof = now;
445    }
446
447    /// Record that an outbound packet was sent (updates timing).
448    pub fn record_outbound(&mut self, now: f64, is_keepalive: bool) {
449        self.last_outbound = now;
450        if is_keepalive {
451            self.last_keepalive = now;
452        }
453    }
454
455    /// Record one accepted inbound packet using its packed data/ciphertext size.
456    pub fn record_inbound_traffic(&mut self, data_len: usize) {
457        if self.state != LinkState::Closed {
458            self.rx_packets = self.rx_packets.saturating_add(1);
459            self.rx_bytes = self.rx_bytes.saturating_add(data_len as u64);
460        }
461    }
462
463    /// Record one successfully packed outbound packet.
464    pub fn record_outbound_traffic(&mut self, data_len: usize) {
465        self.tx_packets = self.tx_packets.saturating_add(1);
466        self.tx_bytes = self.tx_bytes.saturating_add(data_len as u64);
467    }
468
469    /// Periodic tick: check keepalive, stale, timeouts.
470    pub fn tick(&mut self, now: f64) -> Vec<LinkAction> {
471        let mut actions = Vec::new();
472
473        match self.state {
474            LinkState::Pending | LinkState::Handshake => {
475                if is_establishment_timeout(self.request_time, self.establishment_timeout, now) {
476                    self.state = LinkState::Closed;
477                    actions.push(LinkAction::StateChanged {
478                        link_id: self.link_id,
479                        new_state: LinkState::Closed,
480                        reason: Some(TeardownReason::Timeout),
481                    });
482                }
483            }
484            LinkState::Active => {
485                let activated = self.activated_at.unwrap_or(0.0);
486                // Python: max(max(self.last_inbound, self.last_proof), activated_at)
487                let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
488
489                if should_go_stale(last_inbound, self.stale_time, now) {
490                    self.state = LinkState::Stale;
491                    actions.push(LinkAction::StateChanged {
492                        link_id: self.link_id,
493                        new_state: LinkState::Stale,
494                        reason: None,
495                    });
496                }
497            }
498            LinkState::Stale => {
499                // In Python, STALE immediately sends teardown and closes
500                self.state = LinkState::Closed;
501                actions.push(LinkAction::StateChanged {
502                    link_id: self.link_id,
503                    new_state: LinkState::Closed,
504                    reason: Some(TeardownReason::Timeout),
505                });
506            }
507            LinkState::Closed => {}
508        }
509
510        actions
511    }
512
513    /// Check if a keepalive should be sent. Returns true if conditions are met.
514    pub fn needs_keepalive(&self, now: f64) -> bool {
515        if self.state != LinkState::Active || !self.is_initiator {
516            return false;
517        }
518        let activated = self.activated_at.unwrap_or(0.0);
519        let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
520
521        // Initiators probe whenever either direction has been quiet for a full
522        // interval. In particular, continuous responder-to-initiator traffic
523        // must not suppress the probe that keeps the responder's inbound timer
524        // alive.
525        let inbound_quiet = now >= last_inbound + self.keepalive_interval;
526        let outbound_quiet = now >= self.last_outbound + self.keepalive_interval;
527        if !inbound_quiet && !outbound_quiet {
528            return false;
529        }
530
531        should_send_keepalive(self.last_keepalive, self.keepalive_interval, now)
532    }
533
534    /// Return whether a received keepalive probe needs a responder reply.
535    ///
536    /// `0xff` is the initiator probe and `0xfe` the responder acknowledgement,
537    /// matching upstream Reticulum. A responder that sent any traffic within
538    /// the current keepalive interval can omit the acknowledgement because that
539    /// traffic already refreshed the initiator's inbound timer.
540    pub fn should_reply_keepalive(&self, payload: &[u8], now: f64) -> bool {
541        self.state == LinkState::Active
542            && !self.is_initiator
543            && payload == [0xff]
544            && now >= self.last_outbound + self.keepalive_interval
545    }
546
547    /// Tear down the link (initiator-initiated close).
548    pub fn teardown(&mut self) -> Vec<LinkAction> {
549        if self.state == LinkState::Closed {
550            return Vec::new();
551        }
552        self.state = LinkState::Closed;
553        let reason = if self.is_initiator {
554            TeardownReason::InitiatorClosed
555        } else {
556            TeardownReason::DestinationClosed
557        };
558        alloc::vec![LinkAction::StateChanged {
559            link_id: self.link_id,
560            new_state: LinkState::Closed,
561            reason: Some(reason),
562        }]
563    }
564
565    /// Handle incoming teardown (remote close).
566    pub fn handle_teardown(&mut self) -> Vec<LinkAction> {
567        if self.state == LinkState::Closed {
568            return Vec::new();
569        }
570        self.state = LinkState::Closed;
571        let reason = if self.is_initiator {
572            TeardownReason::DestinationClosed
573        } else {
574            TeardownReason::InitiatorClosed
575        };
576        alloc::vec![LinkAction::StateChanged {
577            link_id: self.link_id,
578            new_state: LinkState::Closed,
579            reason: Some(reason),
580        }]
581    }
582
583    // --- Queries ---
584
585    pub fn link_id(&self) -> &LinkId {
586        &self.link_id
587    }
588
589    pub fn state(&self) -> LinkState {
590        self.state
591    }
592
593    pub fn rtt(&self) -> Option<f64> {
594        self.rtt
595    }
596
597    pub fn mdu(&self) -> usize {
598        self.mdu
599    }
600
601    pub fn mtu(&self) -> u32 {
602        self.mtu
603    }
604
605    pub fn is_initiator(&self) -> bool {
606        self.is_initiator
607    }
608
609    pub fn mode(&self) -> LinkMode {
610        self.mode
611    }
612
613    pub fn remote_identity(&self) -> Option<&([u8; 16], [u8; 64])> {
614        self.remote_identity.as_ref()
615    }
616
617    pub fn destination_hash(&self) -> &[u8; 16] {
618        &self.destination_hash
619    }
620
621    pub fn expected_hops(&self) -> u8 {
622        self.expected_hops
623    }
624
625    pub fn rebalanced_at(&self) -> Option<f64> {
626        self.rebalanced_at
627    }
628
629    pub fn tx_packets(&self) -> u64 {
630        self.tx_packets
631    }
632
633    pub fn rx_packets(&self) -> u64 {
634        self.rx_packets
635    }
636
637    pub fn tx_bytes(&self) -> u64 {
638        self.tx_bytes
639    }
640
641    pub fn rx_bytes(&self) -> u64 {
642        self.rx_bytes
643    }
644
645    /// Get the derived session key (needed for hole-punch token derivation).
646    pub fn derived_key(&self) -> Option<&[u8]> {
647        self.derived_key.as_deref()
648    }
649
650    pub fn keepalive_interval(&self) -> f64 {
651        self.keepalive_interval
652    }
653
654    /// Update the measured RTT (e.g., after path redirect to a direct link).
655    /// Also recalculates keepalive and stale timers.
656    pub fn set_rtt(&mut self, rtt: f64) {
657        self.rtt = Some(rtt);
658        self.update_keepalive();
659    }
660
661    /// Update the link MTU (e.g., after path redirect to a different interface).
662    pub fn set_mtu(&mut self, mtu: u32) {
663        self.mtu = mtu;
664        self.mdu = compute_mdu(mtu as usize);
665    }
666
667    #[doc(hidden)]
668    pub fn clear_session_for_testing(&mut self) {
669        self.derived_key = None;
670        self.token = None;
671    }
672
673    // --- Internal ---
674
675    fn update_keepalive(&mut self) {
676        if let Some(rtt) = self.rtt {
677            self.keepalive_interval = compute_keepalive(rtt);
678            self.stale_time = compute_stale_time(self.keepalive_interval);
679        }
680    }
681}
682
683/// Compute link MDU from MTU.
684///
685/// MDU = floor((mtu - IFAC_MIN_SIZE - HEADER_MINSIZE - TOKEN_OVERHEAD) / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
686fn compute_mdu(mtu: usize) -> usize {
687    use crate::constants::{AES128_BLOCKSIZE, HEADER_MINSIZE, IFAC_MIN_SIZE, TOKEN_OVERHEAD};
688    let numerator = mtu.saturating_sub(IFAC_MIN_SIZE + HEADER_MINSIZE + TOKEN_OVERHEAD);
689    (numerator / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695    use crate::constants::LINK_MDU;
696    use rns_crypto::FixedRng;
697
698    fn make_rng(seed: u8) -> FixedRng {
699        FixedRng::new(&[seed; 128])
700    }
701
702    fn active_link_pair() -> (LinkEngine, LinkEngine, Vec<u8>) {
703        let mut rng_id = make_rng(0x01);
704        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
705        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
706        let dest_hash = [0xDD; 16];
707
708        let mut rng_init = make_rng(0x10);
709        let (mut initiator, request_data) = LinkEngine::new_initiator(
710            &dest_hash,
711            1,
712            LinkMode::Aes256Cbc,
713            Some(500),
714            100.0,
715            &mut rng_init,
716        );
717        let mut hashable = vec![0x00, 0x00];
718        hashable.extend_from_slice(&dest_hash);
719        hashable.push(0x00);
720        hashable.extend_from_slice(&request_data);
721        initiator.set_link_id_from_hashable(&hashable, request_data.len());
722
723        let mut rng_resp = make_rng(0x20);
724        let (mut responder, lrproof_data) = LinkEngine::new_responder(
725            &dest_sig_prv,
726            &dest_sig_pub_bytes,
727            &request_data,
728            &hashable,
729            &dest_hash,
730            1,
731            100.5,
732            &mut rng_resp,
733        )
734        .unwrap();
735        let mut rng_lrrtt = make_rng(0x30);
736        let (lrrtt_encrypted, _) = initiator
737            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
738            .unwrap();
739        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
740
741        (initiator, responder, lrrtt_encrypted)
742    }
743
744    #[test]
745    fn test_compute_mdu_default() {
746        assert_eq!(compute_mdu(500), LINK_MDU);
747    }
748
749    #[test]
750    fn test_full_handshake() {
751        // Setup: destination identity (for responder)
752        let mut rng_id = make_rng(0x01);
753        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
754        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
755
756        let dest_hash = [0xDD; 16];
757        let mode = LinkMode::Aes256Cbc;
758
759        // Step 1: Initiator creates link request
760        let mut rng_init = make_rng(0x10);
761        let (mut initiator, request_data) =
762            LinkEngine::new_initiator(&dest_hash, 1, mode, Some(500), 100.0, &mut rng_init);
763        assert_eq!(initiator.state(), LinkState::Pending);
764
765        // Simulate packet packing: build a fake hashable part
766        // In real usage, the caller packs a LINKREQUEST packet and calls set_link_id_from_hashable
767        let mut hashable = Vec::new();
768        hashable.push(0x00); // flags byte (lower nibble)
769        hashable.push(0x00); // hops
770        hashable.extend_from_slice(&dest_hash);
771        hashable.push(0x00); // context
772        hashable.extend_from_slice(&request_data);
773
774        initiator.set_link_id_from_hashable(&hashable, request_data.len());
775        assert_ne!(initiator.link_id(), &[0u8; 16]);
776
777        // Step 2: Responder receives link request
778        let mut rng_resp = make_rng(0x20);
779        let (mut responder, lrproof_data) = LinkEngine::new_responder(
780            &dest_sig_prv,
781            &dest_sig_pub_bytes,
782            &request_data,
783            &hashable,
784            &dest_hash,
785            1,
786            100.5,
787            &mut rng_resp,
788        )
789        .unwrap();
790        assert_eq!(responder.state(), LinkState::Handshake);
791        assert_eq!(responder.link_id(), initiator.link_id());
792
793        // Step 3: Initiator validates LRPROOF
794        let mut rng_lrrtt = make_rng(0x30);
795        assert!(initiator
796            .handle_lrproof_with_hops(
797                &[0xAA; 3],
798                &dest_sig_pub_bytes,
799                Some(8),
800                100.6,
801                &mut rng_lrrtt,
802            )
803            .is_err());
804        assert_eq!(initiator.expected_hops(), 1);
805        let mismatched_mode_proof = handshake::build_lrproof(
806            initiator.link_id(),
807            lrproof_data[64..96].try_into().unwrap(),
808            &dest_sig_pub_bytes,
809            &dest_sig_prv,
810            Some(500),
811            LinkMode::Aes128Cbc,
812        );
813        assert_eq!(
814            initiator
815                .handle_lrproof_with_hops(
816                    &mismatched_mode_proof,
817                    &dest_sig_pub_bytes,
818                    Some(9),
819                    100.65,
820                    &mut rng_lrrtt,
821                )
822                .unwrap_err(),
823            LinkError::UnsupportedMode,
824        );
825        assert_eq!(initiator.expected_hops(), 1);
826        let mut invalid_lrproof = lrproof_data.clone();
827        invalid_lrproof[0] ^= 0x01;
828        assert!(initiator
829            .handle_lrproof_with_hops(
830                &invalid_lrproof,
831                &dest_sig_pub_bytes,
832                Some(7),
833                100.7,
834                &mut rng_lrrtt,
835            )
836            .is_err());
837        assert_eq!(initiator.expected_hops(), 1);
838        let (lrrtt_encrypted, actions) = initiator
839            .handle_lrproof_with_hops(
840                &lrproof_data,
841                &dest_sig_pub_bytes,
842                Some(4),
843                100.8,
844                &mut rng_lrrtt,
845            )
846            .unwrap();
847        assert_eq!(initiator.state(), LinkState::Active);
848        assert!(initiator.rtt().is_some());
849        assert_eq!(initiator.rebalanced_at(), Some(100.8));
850        assert_eq!(actions.len(), 2); // StateChanged + LinkEstablished
851
852        // Step 4: Responder handles LRRTT
853        let actions = responder
854            .handle_lrrtt_with_hops(&lrrtt_encrypted, Some(4), 101.0)
855            .unwrap();
856        assert_eq!(responder.state(), LinkState::Active);
857        assert_eq!(initiator.expected_hops(), 4);
858        assert_eq!(responder.expected_hops(), 4);
859        assert_eq!(responder.rebalanced_at(), None);
860
861        // A later valid proof cannot rewrite the first authenticated
862        // rebalance, even if it reports another hop metric.
863        initiator.state = LinkState::Pending;
864        let mut repeated_rng = make_rng(0x31);
865        initiator
866            .handle_lrproof_with_hops(
867                &lrproof_data,
868                &dest_sig_pub_bytes,
869                Some(6),
870                101.1,
871                &mut repeated_rng,
872            )
873            .unwrap();
874        assert_eq!(initiator.expected_hops(), 4);
875        assert_eq!(initiator.rebalanced_at(), Some(100.8));
876
877        initiator.record_outbound_traffic(48);
878        initiator.record_inbound_traffic(32);
879        assert_eq!((initiator.tx_packets(), initiator.tx_bytes()), (1, 48));
880        assert_eq!((initiator.rx_packets(), initiator.rx_bytes()), (1, 32));
881        assert!(responder.rtt().is_some());
882        assert_eq!(actions.len(), 2);
883    }
884
885    #[test]
886    fn test_encrypt_decrypt_after_handshake() {
887        let mut rng_id = make_rng(0x01);
888        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
889        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
890        let dest_hash = [0xDD; 16];
891
892        let mut rng_init = make_rng(0x10);
893        let (mut initiator, request_data) = LinkEngine::new_initiator(
894            &dest_hash,
895            1,
896            LinkMode::Aes256Cbc,
897            Some(500),
898            100.0,
899            &mut rng_init,
900        );
901        let mut hashable = Vec::new();
902        hashable.push(0x00);
903        hashable.push(0x00);
904        hashable.extend_from_slice(&dest_hash);
905        hashable.push(0x00);
906        hashable.extend_from_slice(&request_data);
907        initiator.set_link_id_from_hashable(&hashable, request_data.len());
908
909        let mut rng_resp = make_rng(0x20);
910        let (mut responder, lrproof_data) = LinkEngine::new_responder(
911            &dest_sig_prv,
912            &dest_sig_pub_bytes,
913            &request_data,
914            &hashable,
915            &dest_hash,
916            1,
917            100.5,
918            &mut rng_resp,
919        )
920        .unwrap();
921
922        let mut rng_lrrtt = make_rng(0x30);
923        let (lrrtt_encrypted, _) = initiator
924            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
925            .unwrap();
926        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
927
928        // Now both sides are ACTIVE — test encrypt/decrypt
929        let mut rng_enc = make_rng(0x40);
930        let plaintext = b"Hello over encrypted link!";
931        let ciphertext = initiator.encrypt(plaintext, &mut rng_enc).unwrap();
932        let decrypted = responder.decrypt(&ciphertext).unwrap();
933        assert_eq!(decrypted, plaintext);
934
935        // And in reverse
936        let mut rng_enc2 = make_rng(0x50);
937        let ciphertext2 = responder.encrypt(b"Reply!", &mut rng_enc2).unwrap();
938        let decrypted2 = initiator.decrypt(&ciphertext2).unwrap();
939        assert_eq!(decrypted2, b"Reply!");
940    }
941
942    #[test]
943    fn test_tick_establishment_timeout() {
944        let mut rng = make_rng(0x10);
945        let dest_hash = [0xDD; 16];
946        let (mut engine, _) =
947            LinkEngine::new_initiator(&dest_hash, 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
948        // Timeout = 6.0 + 6.0 * 1 = 12.0s → expires at 112.0
949
950        // Before timeout — no state change
951        let actions = engine.tick(110.0);
952        assert!(actions.is_empty());
953
954        // After timeout
955        let actions = engine.tick(113.0);
956        assert_eq!(actions.len(), 1);
957        assert_eq!(engine.state(), LinkState::Closed);
958    }
959
960    #[test]
961    fn test_tick_stale_and_close() {
962        let mut rng_id = make_rng(0x01);
963        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
964        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
965        let dest_hash = [0xDD; 16];
966
967        let mut rng_init = make_rng(0x10);
968        let (mut initiator, request_data) = LinkEngine::new_initiator(
969            &dest_hash,
970            1,
971            LinkMode::Aes256Cbc,
972            Some(500),
973            100.0,
974            &mut rng_init,
975        );
976        let mut hashable = Vec::new();
977        hashable.push(0x00);
978        hashable.push(0x00);
979        hashable.extend_from_slice(&dest_hash);
980        hashable.push(0x00);
981        hashable.extend_from_slice(&request_data);
982        initiator.set_link_id_from_hashable(&hashable, request_data.len());
983
984        let mut rng_resp = make_rng(0x20);
985        let (_, lrproof_data) = LinkEngine::new_responder(
986            &dest_sig_prv,
987            &dest_sig_pub_bytes,
988            &request_data,
989            &hashable,
990            &dest_hash,
991            1,
992            100.5,
993            &mut rng_resp,
994        )
995        .unwrap();
996
997        let mut rng_lrrtt = make_rng(0x30);
998        initiator
999            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1000            .unwrap();
1001        assert_eq!(initiator.state(), LinkState::Active);
1002
1003        // Advance time past stale_time
1004        let stale_time = initiator.stale_time;
1005        let actions = initiator.tick(100.8 + stale_time + 1.0);
1006        assert_eq!(initiator.state(), LinkState::Stale);
1007        assert_eq!(actions.len(), 1);
1008
1009        // Next tick: STALE → CLOSED
1010        let actions = initiator.tick(100.8 + stale_time + 2.0);
1011        assert_eq!(initiator.state(), LinkState::Closed);
1012        assert_eq!(actions.len(), 1);
1013    }
1014
1015    #[test]
1016    fn test_needs_keepalive() {
1017        let mut rng_id = make_rng(0x01);
1018        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1019        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1020        let dest_hash = [0xDD; 16];
1021
1022        let mut rng_init = make_rng(0x10);
1023        let (mut initiator, request_data) = LinkEngine::new_initiator(
1024            &dest_hash,
1025            1,
1026            LinkMode::Aes256Cbc,
1027            Some(500),
1028            100.0,
1029            &mut rng_init,
1030        );
1031        let mut hashable = Vec::new();
1032        hashable.push(0x00);
1033        hashable.push(0x00);
1034        hashable.extend_from_slice(&dest_hash);
1035        hashable.push(0x00);
1036        hashable.extend_from_slice(&request_data);
1037        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1038
1039        let mut rng_resp = make_rng(0x20);
1040        let (_, lrproof_data) = LinkEngine::new_responder(
1041            &dest_sig_prv,
1042            &dest_sig_pub_bytes,
1043            &request_data,
1044            &hashable,
1045            &dest_hash,
1046            1,
1047            100.5,
1048            &mut rng_resp,
1049        )
1050        .unwrap();
1051
1052        let mut rng_lrrtt = make_rng(0x30);
1053        initiator
1054            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1055            .unwrap();
1056
1057        let ka = initiator.keepalive_interval();
1058        // Not yet
1059        assert!(!initiator.needs_keepalive(100.8 + ka - 1.0));
1060        // Past keepalive
1061        assert!(initiator.needs_keepalive(100.8 + ka + 1.0));
1062    }
1063
1064    #[test]
1065    fn initiator_probes_when_outbound_is_quiet_despite_recent_inbound() {
1066        let (mut initiator, _, _) = active_link_pair();
1067        let keepalive = initiator.keepalive_interval();
1068        let now = 1_000.0;
1069
1070        initiator.record_inbound(now - 0.1);
1071        initiator.record_outbound(now - keepalive - 0.1, false);
1072
1073        assert!(initiator.needs_keepalive(now));
1074    }
1075
1076    #[test]
1077    fn initiator_probes_when_inbound_is_quiet_despite_recent_outbound() {
1078        let (mut initiator, _, _) = active_link_pair();
1079        let keepalive = initiator.keepalive_interval();
1080        let now = 1_000.0;
1081
1082        initiator.record_inbound(now - keepalive - 0.1);
1083        initiator.record_outbound(now - 0.1, false);
1084
1085        assert!(initiator.needs_keepalive(now));
1086    }
1087
1088    #[test]
1089    fn initiator_does_not_probe_when_both_directions_are_recent() {
1090        let (mut initiator, _, _) = active_link_pair();
1091        let now = 1_000.0;
1092
1093        initiator.record_inbound(now - 0.1);
1094        initiator.record_outbound(now - 0.1, false);
1095
1096        assert!(!initiator.needs_keepalive(now));
1097    }
1098
1099    #[test]
1100    fn test_needs_keepalive_responder() {
1101        let mut rng_id = make_rng(0x01);
1102        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1103        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1104        let dest_hash = [0xDD; 16];
1105
1106        let mut rng_init = make_rng(0x10);
1107        let (mut initiator, request_data) = LinkEngine::new_initiator(
1108            &dest_hash,
1109            1,
1110            LinkMode::Aes256Cbc,
1111            Some(500),
1112            100.0,
1113            &mut rng_init,
1114        );
1115        let mut hashable = Vec::new();
1116        hashable.push(0x00);
1117        hashable.push(0x00);
1118        hashable.extend_from_slice(&dest_hash);
1119        hashable.push(0x00);
1120        hashable.extend_from_slice(&request_data);
1121        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1122
1123        let mut rng_resp = make_rng(0x20);
1124        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1125            &dest_sig_prv,
1126            &dest_sig_pub_bytes,
1127            &request_data,
1128            &hashable,
1129            &dest_hash,
1130            1,
1131            100.5,
1132            &mut rng_resp,
1133        )
1134        .unwrap();
1135
1136        let mut rng_lrrtt = make_rng(0x30);
1137        let (lrrtt_encrypted, _) = initiator
1138            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1139            .unwrap();
1140        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1141
1142        let ka = responder.keepalive_interval();
1143        // Only the initiator emits probes. The responder replies to probes.
1144        assert!(!responder.needs_keepalive(101.0 + ka - 1.0));
1145        assert!(!responder.needs_keepalive(101.0 + ka + 1.0));
1146    }
1147
1148    #[test]
1149    fn responder_replies_only_to_probe_and_only_when_outbound_is_quiet() {
1150        let (_, mut responder, _) = active_link_pair();
1151        let keepalive = responder.keepalive_interval();
1152        let now = 1_000.0;
1153
1154        responder.record_outbound(now - keepalive - 0.1, false);
1155        assert!(responder.should_reply_keepalive(&[0xff], now));
1156        assert!(!responder.should_reply_keepalive(&[0xfe], now));
1157        assert!(!responder.should_reply_keepalive(&[], now));
1158
1159        responder.record_outbound(now - 0.1, false);
1160        assert!(!responder.should_reply_keepalive(&[0xff], now));
1161    }
1162
1163    #[test]
1164    fn initiator_never_replies_to_keepalive_probe() {
1165        let (mut initiator, _, _) = active_link_pair();
1166        let keepalive = initiator.keepalive_interval();
1167        let now = 1_000.0;
1168        initiator.record_outbound(now - keepalive - 0.1, false);
1169
1170        assert!(!initiator.should_reply_keepalive(&[0xff], now));
1171    }
1172
1173    #[test]
1174    fn test_teardown() {
1175        let mut rng = make_rng(0x10);
1176        let (mut engine, _) =
1177            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1178        let actions = engine.teardown();
1179        assert_eq!(engine.state(), LinkState::Closed);
1180        assert_eq!(actions.len(), 1);
1181
1182        // Teardown again is no-op
1183        let actions = engine.teardown();
1184        assert!(actions.is_empty());
1185    }
1186
1187    #[test]
1188    fn test_handle_teardown() {
1189        let mut rng = make_rng(0x10);
1190        let (mut engine, _) =
1191            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1192        let actions = engine.handle_teardown();
1193        assert_eq!(engine.state(), LinkState::Closed);
1194        assert_eq!(actions.len(), 1);
1195        match &actions[0] {
1196            LinkAction::StateChanged { reason, .. } => {
1197                assert_eq!(*reason, Some(TeardownReason::DestinationClosed));
1198            }
1199            _ => panic!("Expected StateChanged"),
1200        }
1201    }
1202
1203    #[test]
1204    fn test_identify_over_link() {
1205        let mut rng_id = make_rng(0x01);
1206        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1207        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1208        let dest_hash = [0xDD; 16];
1209
1210        let mut rng_init = make_rng(0x10);
1211        let (mut initiator, request_data) = LinkEngine::new_initiator(
1212            &dest_hash,
1213            1,
1214            LinkMode::Aes256Cbc,
1215            Some(500),
1216            100.0,
1217            &mut rng_init,
1218        );
1219        let mut hashable = Vec::new();
1220        hashable.push(0x00);
1221        hashable.push(0x00);
1222        hashable.extend_from_slice(&dest_hash);
1223        hashable.push(0x00);
1224        hashable.extend_from_slice(&request_data);
1225        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1226
1227        let mut rng_resp = make_rng(0x20);
1228        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1229            &dest_sig_prv,
1230            &dest_sig_pub_bytes,
1231            &request_data,
1232            &hashable,
1233            &dest_hash,
1234            1,
1235            100.5,
1236            &mut rng_resp,
1237        )
1238        .unwrap();
1239
1240        let mut rng_lrrtt = make_rng(0x30);
1241        let (lrrtt_encrypted, _) = initiator
1242            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1243            .unwrap();
1244        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1245
1246        // Create identity to identify with
1247        let mut rng_ident = make_rng(0x40);
1248        let my_identity = rns_crypto::identity::Identity::new(&mut rng_ident);
1249
1250        // Initiator identifies itself to responder
1251        let mut rng_enc = make_rng(0x50);
1252        let identify_encrypted = initiator
1253            .build_identify(&my_identity, &mut rng_enc)
1254            .unwrap();
1255
1256        let actions = responder.handle_identify(&identify_encrypted).unwrap();
1257        assert_eq!(actions.len(), 1);
1258        match &actions[0] {
1259            LinkAction::RemoteIdentified {
1260                identity_hash,
1261                public_key,
1262                ..
1263            } => {
1264                assert_eq!(identity_hash, my_identity.hash());
1265                assert_eq!(public_key, &my_identity.get_public_key().unwrap());
1266            }
1267            _ => panic!("Expected RemoteIdentified"),
1268        }
1269
1270        // Valid repeated identifies are authenticated, but identification is
1271        // one-shot and cannot replace the first identity or emit callbacks.
1272        let mut rng_repeat = make_rng(0x51);
1273        let repeated = initiator
1274            .build_identify(&my_identity, &mut rng_repeat)
1275            .unwrap();
1276        assert!(responder.handle_identify(&repeated).unwrap().is_empty());
1277
1278        let mut rng_other = make_rng(0x60);
1279        let other_identity = rns_crypto::identity::Identity::new(&mut rng_other);
1280        let mut rng_other_enc = make_rng(0x61);
1281        let other = initiator
1282            .build_identify(&other_identity, &mut rng_other_enc)
1283            .unwrap();
1284        assert!(responder.handle_identify(&other).unwrap().is_empty());
1285        assert_eq!(
1286            responder.remote_identity().map(|(hash, _)| hash),
1287            Some(my_identity.hash())
1288        );
1289
1290        // Malformed repeats are still rejected cryptographically.
1291        let mut rng_bad = make_rng(0x70);
1292        let malformed = initiator
1293            .encrypt(b"invalid identify", &mut rng_bad)
1294            .unwrap();
1295        assert!(responder.handle_identify(&malformed).is_err());
1296        assert_eq!(
1297            responder.remote_identity().map(|(hash, _)| hash),
1298            Some(my_identity.hash())
1299        );
1300    }
1301
1302    #[test]
1303    fn test_aes128_mode_handshake() {
1304        let mut rng_id = make_rng(0x01);
1305        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1306        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1307        let dest_hash = [0xDD; 16];
1308
1309        let mut rng_init = make_rng(0x10);
1310        let (mut initiator, request_data) = LinkEngine::new_initiator(
1311            &dest_hash,
1312            1,
1313            LinkMode::Aes128Cbc,
1314            Some(500),
1315            100.0,
1316            &mut rng_init,
1317        );
1318        let mut hashable = Vec::new();
1319        hashable.push(0x00);
1320        hashable.push(0x00);
1321        hashable.extend_from_slice(&dest_hash);
1322        hashable.push(0x00);
1323        hashable.extend_from_slice(&request_data);
1324        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1325
1326        let mut rng_resp = make_rng(0x20);
1327        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1328            &dest_sig_prv,
1329            &dest_sig_pub_bytes,
1330            &request_data,
1331            &hashable,
1332            &dest_hash,
1333            1,
1334            100.5,
1335            &mut rng_resp,
1336        )
1337        .unwrap();
1338
1339        let mut rng_lrrtt = make_rng(0x30);
1340        let (lrrtt_encrypted, _) = initiator
1341            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1342            .unwrap();
1343        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1344
1345        assert_eq!(initiator.state(), LinkState::Active);
1346        assert_eq!(responder.state(), LinkState::Active);
1347        assert_eq!(initiator.mode(), LinkMode::Aes128Cbc);
1348
1349        // Verify encrypt/decrypt works
1350        let mut rng_enc = make_rng(0x40);
1351        let ct = initiator.encrypt(b"AES128 test", &mut rng_enc).unwrap();
1352        let pt = responder.decrypt(&ct).unwrap();
1353        assert_eq!(pt, b"AES128 test");
1354    }
1355}
1356use alloc::vec;