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, LINK_MTU_MAX,
16    LINK_MTU_MIN, MTU, PATHFINDER_M,
17};
18
19pub use types::{LinkAction, LinkError, LinkId, LinkMode, LinkState, TeardownReason};
20
21use crypto::{create_session_token, link_decrypt, link_decrypt_chunks, link_encrypt};
22use handshake::{
23    build_linkrequest_data, compute_link_id, pack_rtt, parse_linkrequest_data,
24    perform_key_exchange, unpack_rtt, validate_lrproof,
25};
26use keepalive::{
27    compute_establishment_timeout, compute_keepalive, compute_stale_time, is_establishment_timeout,
28    should_go_stale, should_send_keepalive, stale_close_timeout,
29};
30
31/// The Link Engine manages a single link's lifecycle.
32///
33/// It follows the action-queue model: methods return `Vec<LinkAction>` instead
34/// of performing I/O directly. The caller dispatches actions.
35pub struct LinkEngine {
36    link_id: LinkId,
37    state: LinkState,
38    is_initiator: bool,
39    mode: LinkMode,
40
41    // Ephemeral keys
42    prv: X25519PrivateKey,
43    sig_prv: Ed25519PrivateKey,
44
45    // Peer keys
46    peer_pub_bytes: Option<[u8; 32]>,
47    peer_sig_pub_bytes: Option<[u8; 32]>,
48    /// The peer's signing key, parsed once instead of for every proof.
49    peer_sig_pub: Option<Ed25519PublicKey>,
50
51    // Session crypto
52    derived_key: Option<Vec<u8>>,
53    token: Option<Token>,
54
55    // Timing
56    request_time: f64,
57    activated_at: Option<f64>,
58    last_inbound: f64,
59    last_outbound: f64,
60    last_keepalive: f64,
61    last_proof: f64,
62    rtt: Option<f64>,
63    keepalive_interval: f64,
64    stale_time: f64,
65    /// When the link went stale; it closes `stale_close_timeout` later
66    /// unless inbound traffic recovers it first.
67    stale_since: Option<f64>,
68    establishment_timeout: f64,
69    expected_hops: u8,
70    /// Time of the first authenticated terminus hop rebalance.
71    rebalanced_at: Option<f64>,
72
73    // Counts packed packet data/ciphertext, matching Python Link accounting.
74    tx_packets: u64,
75    rx_packets: u64,
76    tx_bytes: u64,
77    rx_bytes: u64,
78
79    // Identity
80    remote_identity: Option<([u8; 16], [u8; 64])>,
81    destination_hash: [u8; 16],
82
83    // MDU
84    mtu: u32,
85    mdu: usize,
86}
87
88impl LinkEngine {
89    /// Create a new initiator-side link engine.
90    ///
91    /// Returns `(engine, linkrequest_data)` — the caller must pack linkrequest_data
92    /// into a LINKREQUEST packet and send it.
93    pub fn new_initiator(
94        dest_hash: &[u8; 16],
95        hops: u8,
96        mode: LinkMode,
97        mtu: Option<u32>,
98        now: f64,
99        rng: &mut dyn Rng,
100    ) -> (Self, Vec<u8>) {
101        let prv = X25519PrivateKey::generate(rng);
102        let sig_prv = Ed25519PrivateKey::generate(rng);
103        Self::new_initiator_with_keys(dest_hash, hops, mode, mtu, now, prv, sig_prv)
104    }
105
106    /// Create an initiator link whose X25519 public key carries a stamp of at
107    /// least `stamp_cost` over `destination_hash || ed25519_public_key`.
108    pub fn new_initiator_stamped(
109        dest_hash: &[u8; 16],
110        hops: u8,
111        mode: LinkMode,
112        mtu: Option<u32>,
113        now: f64,
114        rng: &mut dyn Rng,
115        stamp_cost: crate::stamp::StampCost,
116    ) -> (Self, Vec<u8>) {
117        let sig_prv = Ed25519PrivateKey::generate(rng);
118        let sig_pub_bytes = sig_prv.public_key().public_bytes();
119        let mut material = [0u8; 48];
120        material[..16].copy_from_slice(dest_hash);
121        material[16..].copy_from_slice(&sig_pub_bytes);
122        let (prv, _public, _value) =
123            rns_crypto::identity::generate_stamped_key(rng, &material, stamp_cost);
124        Self::new_initiator_with_keys(dest_hash, hops, mode, mtu, now, prv, sig_prv)
125    }
126
127    /// Create an initiator link from pre-generated keys.
128    ///
129    /// Used when the X25519 key was stamped ahead of time, off the caller's
130    /// latency-sensitive thread; see `rns-net`'s link request stamping.
131    #[allow(clippy::too_many_arguments)]
132    pub fn new_initiator_with_keys(
133        dest_hash: &[u8; 16],
134        hops: u8,
135        mode: LinkMode,
136        mtu: Option<u32>,
137        now: f64,
138        prv: X25519PrivateKey,
139        sig_prv: Ed25519PrivateKey,
140    ) -> (Self, Vec<u8>) {
141        let pub_bytes = prv.public_key().public_bytes();
142        let sig_pub_bytes = sig_prv.public_key().public_bytes();
143
144        let request_data = build_linkrequest_data(&pub_bytes, &sig_pub_bytes, mtu, mode);
145
146        let link_mtu = effective_link_mtu(mtu);
147
148        let engine = LinkEngine {
149            link_id: [0u8; 16], // will be set after packet is built
150            state: LinkState::Pending,
151            is_initiator: true,
152            mode,
153            prv,
154            sig_prv,
155            peer_pub_bytes: None,
156            peer_sig_pub_bytes: None,
157            peer_sig_pub: None,
158            derived_key: None,
159            token: None,
160            request_time: now,
161            activated_at: None,
162            last_inbound: now,
163            last_outbound: now,
164            last_keepalive: now,
165            last_proof: 0.0,
166            rtt: None,
167            keepalive_interval: LINK_KEEPALIVE_MAX,
168            stale_time: LINK_KEEPALIVE_MAX * 2.0,
169            stale_since: None,
170            establishment_timeout: compute_establishment_timeout(
171                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
172                hops,
173            ),
174            expected_hops: if hops < PATHFINDER_M {
175                hops
176            } else {
177                PATHFINDER_M
178            },
179            rebalanced_at: None,
180            tx_packets: 0,
181            rx_packets: 0,
182            tx_bytes: 0,
183            rx_bytes: 0,
184            remote_identity: None,
185            destination_hash: *dest_hash,
186            mtu: link_mtu,
187            mdu: compute_mdu(link_mtu as usize),
188        };
189
190        (engine, request_data)
191    }
192
193    /// Set link_id from the hashable part of the packed LINKREQUEST packet.
194    ///
195    /// Must be called after packing the LINKREQUEST packet (since link_id depends
196    /// on the packet's hashable part).
197    pub fn set_link_id_from_hashable(&mut self, hashable_part: &[u8], data_len: usize) {
198        let extra = data_len.saturating_sub(LINK_ECPUBSIZE);
199        self.link_id = compute_link_id(hashable_part, extra);
200    }
201
202    /// Create a new responder-side link engine from an incoming LINKREQUEST.
203    ///
204    /// `owner_sig_prv` / `owner_sig_pub` are the destination's signing keys.
205    /// Returns `(engine, actions)` where actions include the LRPROOF data to send.
206    #[allow(clippy::too_many_arguments)]
207    pub fn new_responder(
208        owner_sig_prv: &Ed25519PrivateKey,
209        owner_sig_pub_bytes: &[u8; 32],
210        linkrequest_data: &[u8],
211        hashable_part: &[u8],
212        dest_hash: &[u8; 16],
213        hops: u8,
214        now: f64,
215        rng: &mut dyn Rng,
216    ) -> Result<(Self, Vec<u8>), LinkError> {
217        let (peer_pub, peer_sig_pub, peer_mtu, mode) = parse_linkrequest_data(linkrequest_data)?;
218
219        let extra = linkrequest_data.len().saturating_sub(LINK_ECPUBSIZE);
220        let link_id = compute_link_id(hashable_part, extra);
221
222        // Generate ephemeral keys for this end
223        let prv = X25519PrivateKey::generate(rng);
224        let pub_bytes = prv.public_key().public_bytes();
225        let sig_pub_bytes = *owner_sig_pub_bytes;
226
227        // Perform ECDH + HKDF
228        let derived_key = perform_key_exchange(&prv, &peer_pub, &link_id, mode)?;
229        let token = create_session_token(&derived_key)?;
230
231        let link_mtu = effective_link_mtu(peer_mtu);
232
233        // Build LRPROOF
234        let lrproof_data = handshake::build_lrproof(
235            &link_id,
236            &pub_bytes,
237            &sig_pub_bytes,
238            owner_sig_prv,
239            peer_mtu,
240            mode,
241        );
242
243        let engine = LinkEngine {
244            link_id,
245            state: LinkState::Handshake,
246            is_initiator: false,
247            mode,
248            prv,
249            sig_prv: Ed25519PrivateKey::from_bytes(&owner_sig_prv.private_bytes()),
250            peer_pub_bytes: Some(peer_pub),
251            peer_sig_pub_bytes: Some(peer_sig_pub),
252            peer_sig_pub: Ed25519PublicKey::from_bytes(&peer_sig_pub).ok(),
253            derived_key: Some(derived_key),
254            token: Some(token),
255            request_time: now,
256            activated_at: None,
257            last_inbound: now,
258            last_outbound: now,
259            last_keepalive: now,
260            last_proof: 0.0,
261            rtt: None,
262            keepalive_interval: LINK_KEEPALIVE_MAX,
263            stale_time: LINK_KEEPALIVE_MAX * 2.0,
264            stale_since: None,
265            establishment_timeout: compute_establishment_timeout(
266                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
267                hops,
268            ),
269            expected_hops: PATHFINDER_M,
270            rebalanced_at: None,
271            tx_packets: 0,
272            rx_packets: 0,
273            tx_bytes: 0,
274            rx_bytes: 0,
275            remote_identity: None,
276            destination_hash: *dest_hash,
277            mtu: link_mtu,
278            mdu: compute_mdu(link_mtu as usize),
279        };
280
281        Ok((engine, lrproof_data))
282    }
283
284    /// Handle an incoming LRPROOF (initiator side).
285    ///
286    /// Validates the proof, performs ECDH, derives session key, returns LRRTT data
287    /// to be encrypted and sent.
288    pub fn handle_lrproof(
289        &mut self,
290        proof_data: &[u8],
291        peer_sig_pub_bytes: &[u8; 32],
292        now: f64,
293        rng: &mut dyn Rng,
294    ) -> Result<(Vec<u8>, Vec<LinkAction>), LinkError> {
295        self.handle_lrproof_with_hops(proof_data, peer_sig_pub_bytes, None, now, rng)
296    }
297
298    /// Validate an LRPROOF while recording its authenticated hop metric.
299    pub fn handle_lrproof_with_hops(
300        &mut self,
301        proof_data: &[u8],
302        peer_sig_pub_bytes: &[u8; 32],
303        packet_hops: Option<u8>,
304        now: f64,
305        rng: &mut dyn Rng,
306    ) -> Result<(Vec<u8>, Vec<LinkAction>), LinkError> {
307        if self.state != LinkState::Pending || !self.is_initiator {
308            return Err(LinkError::InvalidState);
309        }
310
311        let peer_sig_pub = Ed25519PublicKey::from_bytes(peer_sig_pub_bytes)
312            .map_err(|_| LinkError::InvalidSignature)?;
313
314        let (peer_pub, confirmed_mtu, confirmed_mode) =
315            validate_lrproof(proof_data, &self.link_id, &peer_sig_pub, peer_sig_pub_bytes)?;
316
317        if confirmed_mode != self.mode {
318            return Err(LinkError::UnsupportedMode);
319        }
320
321        if let Some(hops) = packet_hops {
322            if hops != self.expected_hops && self.rebalanced_at.is_none() {
323                self.rebalanced_at = Some(now);
324                self.expected_hops = hops;
325            }
326        }
327
328        self.peer_pub_bytes = Some(peer_pub);
329        self.peer_sig_pub_bytes = Some(*peer_sig_pub_bytes);
330        self.peer_sig_pub = Some(peer_sig_pub);
331
332        // ECDH + HKDF
333        let derived_key = perform_key_exchange(&self.prv, &peer_pub, &self.link_id, self.mode)?;
334        let token = create_session_token(&derived_key)?;
335
336        self.derived_key = Some(derived_key);
337        self.token = Some(token);
338
339        // Update MTU if confirmed
340        if confirmed_mtu.is_some() {
341            let mtu = effective_link_mtu(confirmed_mtu);
342            self.mtu = mtu;
343            self.mdu = compute_mdu(mtu as usize);
344        }
345
346        // Compute RTT and activate
347        let rtt = now - self.request_time;
348        self.rtt = Some(rtt);
349        self.state = LinkState::Active;
350        self.activated_at = Some(now);
351        self.last_inbound = now;
352        self.update_keepalive();
353
354        // Build encrypted LRRTT packet data
355        let rtt_packed = pack_rtt(rtt);
356        let rtt_encrypted = self.encrypt(&rtt_packed, rng)?;
357
358        let actions = vec![
359            LinkAction::StateChanged {
360                link_id: self.link_id,
361                new_state: LinkState::Active,
362                reason: None,
363            },
364            LinkAction::LinkEstablished {
365                link_id: self.link_id,
366                rtt,
367                is_initiator: true,
368            },
369        ];
370
371        Ok((rtt_encrypted, actions))
372    }
373
374    /// Handle an incoming LRRTT (responder side).
375    ///
376    /// Decrypts the RTT packet, activates the link.
377    pub fn handle_lrrtt(
378        &mut self,
379        encrypted_data: &[u8],
380        now: f64,
381    ) -> Result<Vec<LinkAction>, LinkError> {
382        self.handle_lrrtt_with_hops(encrypted_data, None, now)
383    }
384
385    /// Handle LRRTT while recording the responder-side hop metric. The legacy
386    /// timing-only method remains available for existing callers.
387    pub fn handle_lrrtt_with_hops(
388        &mut self,
389        encrypted_data: &[u8],
390        packet_hops: Option<u8>,
391        now: f64,
392    ) -> Result<Vec<LinkAction>, LinkError> {
393        if self.state != LinkState::Handshake || self.is_initiator {
394            return Err(LinkError::InvalidState);
395        }
396
397        let plaintext = self.decrypt(encrypted_data)?;
398        let initiator_rtt = unpack_rtt(&plaintext).ok_or(LinkError::InvalidData)?;
399        if let Some(hops) = packet_hops {
400            self.expected_hops = hops;
401        }
402
403        let measured_rtt = now - self.request_time;
404        // `f64::max` ignores a NaN operand, so a NaN can never be selected.
405        let rtt = measured_rtt.max(initiator_rtt);
406
407        self.rtt = Some(rtt);
408        self.state = LinkState::Active;
409        self.activated_at = Some(now);
410        self.last_inbound = now;
411        self.update_keepalive();
412
413        let actions = vec![
414            LinkAction::StateChanged {
415                link_id: self.link_id,
416                new_state: LinkState::Active,
417                reason: None,
418            },
419            LinkAction::LinkEstablished {
420                link_id: self.link_id,
421                rtt,
422                is_initiator: false,
423            },
424        ];
425
426        Ok(actions)
427    }
428
429    /// Encrypt plaintext for transmission over this link.
430    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Result<Vec<u8>, LinkError> {
431        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
432        Ok(link_encrypt(token, plaintext, rng))
433    }
434
435    /// [`Self::encrypt`] with the tag left to the caller (see
436    /// [`Token::encrypt_untagged`]).
437    pub fn encrypt_untagged(
438        &self,
439        plaintext: &[u8],
440        rng: &mut dyn Rng,
441    ) -> Result<(Vec<u8>, rns_crypto::hmac::HmacSha256), LinkError> {
442        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
443        Ok(token.encrypt_untagged(plaintext, rng))
444    }
445
446    /// Whether this link has a session key to encrypt with.
447    pub fn can_encrypt(&self) -> bool {
448        self.token.is_some()
449    }
450
451    /// Length of the ciphertext [`LinkEngine::encrypt`] produces for a
452    /// plaintext of `plaintext_len` bytes.
453    pub fn encrypted_len(&self, plaintext_len: usize) -> usize {
454        Token::token_len(plaintext_len)
455    }
456
457    /// [`LinkEngine::encrypt`] into `out`, which must be exactly
458    /// [`LinkEngine::encrypted_len`]`(plaintext.len())` bytes long.
459    pub fn encrypt_into(
460        &self,
461        plaintext: &[u8],
462        rng: &mut dyn Rng,
463        out: &mut [u8],
464    ) -> Result<(), LinkError> {
465        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
466        token.encrypt_into(plaintext, rng, out);
467        Ok(())
468    }
469
470    /// Decrypt ciphertext received on this link.
471    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, LinkError> {
472        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
473        link_decrypt(token, ciphertext)
474    }
475
476    /// [`LinkEngine::decrypt`] into `out`, replacing its contents and
477    /// reusing its capacity.
478    pub fn decrypt_into(&self, ciphertext: &[u8], out: &mut Vec<u8>) -> Result<(), LinkError> {
479        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
480        token
481            .decrypt_into(ciphertext, out)
482            .map_err(|_| LinkError::CryptoError)
483    }
484
485    /// Decrypt ciphertext held in consecutive chunks without joining them,
486    /// dropping the first `skip` plaintext bytes.
487    pub fn decrypt_chunks(&self, chunks: &[&[u8]], skip: usize) -> Result<Vec<u8>, LinkError> {
488        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
489        link_decrypt_chunks(token, chunks, skip)
490    }
491
492    /// Start a [`Self::decrypt_chunks`] whose chunks are fed as they arrive.
493    pub fn decrypt_stream(&self, skip: usize) -> Option<rns_crypto::token::TokenDecryptStream> {
494        Some(self.token.as_ref()?.decrypt_stream(skip))
495    }
496
497    /// Sign an explicit delivery proof for a packet sent over this Link.
498    pub fn sign_packet_hash(&self, packet_hash: &[u8; 32]) -> [u8; 64] {
499        self.sig_prv.sign(packet_hash)
500    }
501
502    /// This end's Link signing key, which signs its delivery proofs.
503    pub fn signing_key(&self) -> &Ed25519PrivateKey {
504        &self.sig_prv
505    }
506
507    /// The remote endpoint's Link signing key, once known.
508    pub fn peer_signing_key(&self) -> Option<&Ed25519PublicKey> {
509        self.peer_sig_pub.as_ref()
510    }
511
512    /// Validate a delivery proof using the remote endpoint's Link signing key.
513    pub fn validate_packet_proof(&self, packet_hash: &[u8; 32], signature: &[u8; 64]) -> bool {
514        self.peer_sig_pub
515            .as_ref()
516            .is_some_and(|key| key.verify(signature, packet_hash))
517    }
518
519    /// Build LINKIDENTIFY data (encrypted).
520    pub fn build_identify(
521        &self,
522        identity: &rns_crypto::identity::Identity,
523        rng: &mut dyn Rng,
524    ) -> Result<Vec<u8>, LinkError> {
525        if self.state != LinkState::Active {
526            return Err(LinkError::InvalidState);
527        }
528        let plaintext = identify::build_identify_data(identity, &self.link_id)?;
529        self.encrypt(&plaintext, rng)
530    }
531
532    /// Handle incoming LINKIDENTIFY (encrypted data).
533    ///
534    /// Only responders (non-initiators) can receive LINKIDENTIFY (Python: Link.py:1017).
535    pub fn handle_identify(&mut self, encrypted_data: &[u8]) -> Result<Vec<LinkAction>, LinkError> {
536        if self.state != LinkState::Active || self.is_initiator {
537            return Err(LinkError::InvalidState);
538        }
539
540        let plaintext = self.decrypt(encrypted_data)?;
541        let (identity_hash, public_key) =
542            identify::validate_identify_data(&plaintext, &self.link_id)?;
543        if self.remote_identity.is_some() {
544            return Ok(Vec::new());
545        }
546        self.remote_identity = Some((identity_hash, public_key));
547
548        Ok(alloc::vec![LinkAction::RemoteIdentified {
549            link_id: self.link_id,
550            identity_hash,
551            public_key,
552        }])
553    }
554
555    /// Record that an inbound packet was received (updates timing).
556    ///
557    /// If the link is STALE, recovers to ACTIVE (Python: Link.py:987-988).
558    pub fn record_inbound(&mut self, now: f64) -> Vec<LinkAction> {
559        self.last_inbound = now;
560        if self.state == LinkState::Stale {
561            self.state = LinkState::Active;
562            self.stale_since = None;
563            return alloc::vec![LinkAction::StateChanged {
564                link_id: self.link_id,
565                new_state: LinkState::Active,
566                reason: None,
567            }];
568        }
569        Vec::new()
570    }
571
572    /// Record that a proof signed by the peer was received.
573    ///
574    /// Python's `PacketReceipt.validate_link_proof` sets `link.last_proof`,
575    /// and the watchdog's stale check takes the latest of `last_inbound` and
576    /// `last_proof`: an initiator that only ever hears proofs back for its
577    /// own confirmed traffic stays active. Proofs do not reactivate a link
578    /// that already went stale, matching upstream.
579    pub fn record_proof(&mut self, now: f64) {
580        self.last_proof = now;
581    }
582
583    /// Record that an outbound packet was sent (updates timing).
584    pub fn record_outbound(&mut self, now: f64, is_keepalive: bool) {
585        self.last_outbound = now;
586        if is_keepalive {
587            self.last_keepalive = now;
588        }
589    }
590
591    /// Record one accepted inbound packet using its packed data/ciphertext size.
592    pub fn record_inbound_traffic(&mut self, data_len: usize) {
593        if self.state != LinkState::Closed {
594            self.rx_packets = self.rx_packets.saturating_add(1);
595            self.rx_bytes = self.rx_bytes.saturating_add(data_len as u64);
596        }
597    }
598
599    /// Record one successfully packed outbound packet.
600    pub fn record_outbound_traffic(&mut self, data_len: usize) {
601        self.tx_packets = self.tx_packets.saturating_add(1);
602        self.tx_bytes = self.tx_bytes.saturating_add(data_len as u64);
603    }
604
605    /// Periodic tick: check keepalive, stale, timeouts.
606    pub fn tick(&mut self, now: f64) -> Vec<LinkAction> {
607        let mut actions = Vec::new();
608
609        match self.state {
610            LinkState::Pending | LinkState::Handshake => {
611                if is_establishment_timeout(self.request_time, self.establishment_timeout, now) {
612                    self.state = LinkState::Closed;
613                    actions.push(LinkAction::StateChanged {
614                        link_id: self.link_id,
615                        new_state: LinkState::Closed,
616                        reason: Some(TeardownReason::Timeout),
617                    });
618                }
619            }
620            LinkState::Active => {
621                let activated = self.activated_at.unwrap_or(0.0);
622                // Python: max(max(self.last_inbound, self.last_proof), activated_at)
623                let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
624
625                if should_go_stale(last_inbound, self.stale_time, now) {
626                    self.state = LinkState::Stale;
627                    self.stale_since = Some(now);
628                    actions.push(LinkAction::StateChanged {
629                        link_id: self.link_id,
630                        new_state: LinkState::Stale,
631                        reason: None,
632                    });
633                }
634            }
635            LinkState::Stale => {
636                // Python's watchdog sleeps `rtt * KEEPALIVE_TIMEOUT_FACTOR +
637                // STALE_GRACE` after marking the link stale and only closes
638                // it if no inbound packet reactivated it meanwhile.
639                let rtt = self.rtt.filter(|rtt| rtt.is_finite()).unwrap_or(0.0);
640                let stale_since = self.stale_since.unwrap_or(now);
641                if now >= stale_since + stale_close_timeout(rtt) {
642                    self.state = LinkState::Closed;
643                    self.stale_since = None;
644                    actions.push(LinkAction::StateChanged {
645                        link_id: self.link_id,
646                        new_state: LinkState::Closed,
647                        reason: Some(TeardownReason::Timeout),
648                    });
649                }
650            }
651            LinkState::Closed => {}
652        }
653
654        actions
655    }
656
657    /// Check if a keepalive should be sent. Returns true if conditions are met.
658    pub fn needs_keepalive(&self, now: f64) -> bool {
659        if self.state != LinkState::Active || !self.is_initiator {
660            return false;
661        }
662        let activated = self.activated_at.unwrap_or(0.0);
663        let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
664
665        // Initiators probe whenever either direction has been quiet for a full
666        // interval. In particular, continuous responder-to-initiator traffic
667        // must not suppress the probe that keeps the responder's inbound timer
668        // alive.
669        let inbound_quiet = now >= last_inbound + self.keepalive_interval;
670        let outbound_quiet = now >= self.last_outbound + self.keepalive_interval;
671        if !inbound_quiet && !outbound_quiet {
672            return false;
673        }
674
675        should_send_keepalive(self.last_keepalive, self.keepalive_interval, now)
676    }
677
678    /// Return whether a received keepalive probe needs a responder reply.
679    ///
680    /// `0xff` is the initiator probe and `0xfe` the responder acknowledgement,
681    /// matching upstream Reticulum. A responder that sent any traffic within
682    /// the current keepalive interval can omit the acknowledgement because that
683    /// traffic already refreshed the initiator's inbound timer.
684    pub fn should_reply_keepalive(&self, payload: &[u8], now: f64) -> bool {
685        self.state == LinkState::Active
686            && !self.is_initiator
687            && payload == [0xff]
688            && now >= self.last_outbound + self.keepalive_interval
689    }
690
691    /// Tear down the link (initiator-initiated close).
692    pub fn teardown(&mut self) -> Vec<LinkAction> {
693        if self.state == LinkState::Closed {
694            return Vec::new();
695        }
696        self.state = LinkState::Closed;
697        let reason = if self.is_initiator {
698            TeardownReason::InitiatorClosed
699        } else {
700            TeardownReason::DestinationClosed
701        };
702        alloc::vec![LinkAction::StateChanged {
703            link_id: self.link_id,
704            new_state: LinkState::Closed,
705            reason: Some(reason),
706        }]
707    }
708
709    /// Handle incoming teardown (remote close).
710    pub fn handle_teardown(&mut self) -> Vec<LinkAction> {
711        if self.state == LinkState::Closed {
712            return Vec::new();
713        }
714        self.state = LinkState::Closed;
715        let reason = if self.is_initiator {
716            TeardownReason::DestinationClosed
717        } else {
718            TeardownReason::InitiatorClosed
719        };
720        alloc::vec![LinkAction::StateChanged {
721            link_id: self.link_id,
722            new_state: LinkState::Closed,
723            reason: Some(reason),
724        }]
725    }
726
727    // --- Queries ---
728
729    pub fn link_id(&self) -> &LinkId {
730        &self.link_id
731    }
732
733    pub fn state(&self) -> LinkState {
734        self.state
735    }
736
737    pub fn rtt(&self) -> Option<f64> {
738        self.rtt
739    }
740
741    pub fn mdu(&self) -> usize {
742        self.mdu
743    }
744
745    pub fn mtu(&self) -> u32 {
746        self.mtu
747    }
748
749    pub fn is_initiator(&self) -> bool {
750        self.is_initiator
751    }
752
753    /// When the link became active, or `None` if it never did.
754    pub fn activated_at(&self) -> Option<f64> {
755        self.activated_at
756    }
757
758    pub fn mode(&self) -> LinkMode {
759        self.mode
760    }
761
762    pub fn remote_identity(&self) -> Option<&([u8; 16], [u8; 64])> {
763        self.remote_identity.as_ref()
764    }
765
766    pub fn destination_hash(&self) -> &[u8; 16] {
767        &self.destination_hash
768    }
769
770    pub fn expected_hops(&self) -> u8 {
771        self.expected_hops
772    }
773
774    pub fn rebalanced_at(&self) -> Option<f64> {
775        self.rebalanced_at
776    }
777
778    pub fn tx_packets(&self) -> u64 {
779        self.tx_packets
780    }
781
782    pub fn rx_packets(&self) -> u64 {
783        self.rx_packets
784    }
785
786    pub fn tx_bytes(&self) -> u64 {
787        self.tx_bytes
788    }
789
790    pub fn rx_bytes(&self) -> u64 {
791        self.rx_bytes
792    }
793
794    /// Get the derived session key (needed for hole-punch token derivation).
795    pub fn derived_key(&self) -> Option<&[u8]> {
796        self.derived_key.as_deref()
797    }
798
799    pub fn keepalive_interval(&self) -> f64 {
800        self.keepalive_interval
801    }
802
803    /// When the peer's latest valid packet proof arrived (0 before the first).
804    pub fn last_proof(&self) -> f64 {
805        self.last_proof
806    }
807
808    /// Update the measured RTT (e.g., after path redirect to a direct link).
809    /// Also recalculates keepalive and stale timers.
810    pub fn set_rtt(&mut self, rtt: f64) {
811        self.rtt = Some(rtt);
812        self.update_keepalive();
813    }
814
815    /// Update the link MTU (e.g., after path redirect to a different interface).
816    pub fn set_mtu(&mut self, mtu: u32) {
817        self.mtu = mtu;
818        self.mdu = compute_mdu(mtu as usize);
819    }
820
821    #[doc(hidden)]
822    pub fn clear_session_for_testing(&mut self) {
823        self.derived_key = None;
824        self.token = None;
825    }
826
827    // --- Internal ---
828
829    fn update_keepalive(&mut self) {
830        if let Some(rtt) = self.rtt {
831            self.keepalive_interval = compute_keepalive(rtt);
832            self.stale_time = compute_stale_time(self.keepalive_interval);
833        }
834    }
835}
836
837/// Compute link MDU from MTU.
838///
839/// MDU = floor((mtu - IFAC_MIN_SIZE - HEADER_MINSIZE - TOKEN_OVERHEAD) / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
840///
841/// Saturates at 0 for MTUs too small to carry one AES block.
842fn compute_mdu(mtu: usize) -> usize {
843    use crate::constants::{AES128_BLOCKSIZE, HEADER_MINSIZE, IFAC_MIN_SIZE, TOKEN_OVERHEAD};
844    let numerator = mtu.saturating_sub(IFAC_MIN_SIZE + HEADER_MINSIZE + TOKEN_OVERHEAD);
845    ((numerator / AES128_BLOCKSIZE) * AES128_BLOCKSIZE).saturating_sub(1)
846}
847
848/// Resolve the link MTU from the signalled value: absent or 0 means the
849/// default Reticulum MTU; anything else is clamped to
850/// `[LINK_MTU_MIN, LINK_MTU_MAX]`.
851fn effective_link_mtu(signalled_mtu: Option<u32>) -> u32 {
852    signalled_mtu
853        .filter(|mtu| *mtu != 0)
854        .map(|mtu| mtu.clamp(LINK_MTU_MIN, LINK_MTU_MAX))
855        .unwrap_or(MTU as u32)
856}
857use alloc::vec;
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862    use crate::constants::{LINK_MDU, LINK_STALE_GRACE};
863    use rns_crypto::FixedRng;
864
865    fn make_rng(seed: u8) -> FixedRng {
866        FixedRng::new(&[seed; 128])
867    }
868
869    fn active_link_pair() -> (LinkEngine, LinkEngine, Vec<u8>) {
870        let mut rng_id = make_rng(0x01);
871        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
872        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
873        let dest_hash = [0xDD; 16];
874
875        let mut rng_init = make_rng(0x10);
876        let (mut initiator, request_data) = LinkEngine::new_initiator(
877            &dest_hash,
878            1,
879            LinkMode::Aes256Cbc,
880            Some(500),
881            100.0,
882            &mut rng_init,
883        );
884        let mut hashable = vec![0x00, 0x00];
885        hashable.extend_from_slice(&dest_hash);
886        hashable.push(0x00);
887        hashable.extend_from_slice(&request_data);
888        initiator.set_link_id_from_hashable(&hashable, request_data.len());
889
890        let mut rng_resp = make_rng(0x20);
891        let (mut responder, lrproof_data) = LinkEngine::new_responder(
892            &dest_sig_prv,
893            &dest_sig_pub_bytes,
894            &request_data,
895            &hashable,
896            &dest_hash,
897            1,
898            100.5,
899            &mut rng_resp,
900        )
901        .unwrap();
902        let mut rng_lrrtt = make_rng(0x30);
903        let (lrrtt_encrypted, _) = initiator
904            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
905            .unwrap();
906        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
907
908        (initiator, responder, lrrtt_encrypted)
909    }
910
911    #[test]
912    fn test_compute_mdu_default() {
913        assert_eq!(compute_mdu(500), LINK_MDU);
914    }
915
916    #[test]
917    fn zero_mtu_signalling_falls_back_to_reticulum_mtu() {
918        let mut rng_id = make_rng(0x01);
919        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
920        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
921        let dest_hash = [0xDD; 16];
922
923        let mut rng_init = make_rng(0x10);
924        let (mut initiator, request_data) = LinkEngine::new_initiator(
925            &dest_hash,
926            1,
927            LinkMode::Aes256Cbc,
928            Some(0),
929            100.0,
930            &mut rng_init,
931        );
932        let mut hashable = vec![0x00, 0x00];
933        hashable.extend_from_slice(&dest_hash);
934        hashable.push(0x00);
935        hashable.extend_from_slice(&request_data);
936        initiator.set_link_id_from_hashable(&hashable, request_data.len());
937
938        let mut rng_resp = make_rng(0x20);
939        let (responder, lrproof_data) = LinkEngine::new_responder(
940            &dest_sig_prv,
941            &dest_sig_pub_bytes,
942            &request_data,
943            &hashable,
944            &dest_hash,
945            1,
946            100.5,
947            &mut rng_resp,
948        )
949        .unwrap();
950
951        let mut rng_lrrtt = make_rng(0x30);
952        initiator
953            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
954            .unwrap();
955
956        assert_eq!(initiator.mtu(), MTU as u32);
957        assert_eq!(responder.mtu(), MTU as u32);
958        assert_eq!(initiator.mdu(), LINK_MDU);
959        assert_eq!(responder.mdu(), LINK_MDU);
960    }
961
962    #[test]
963    fn test_full_handshake() {
964        // Setup: destination identity (for responder)
965        let mut rng_id = make_rng(0x01);
966        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
967        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
968
969        let dest_hash = [0xDD; 16];
970        let mode = LinkMode::Aes256Cbc;
971
972        // Step 1: Initiator creates link request
973        let mut rng_init = make_rng(0x10);
974        let (mut initiator, request_data) =
975            LinkEngine::new_initiator(&dest_hash, 1, mode, Some(500), 100.0, &mut rng_init);
976        assert_eq!(initiator.state(), LinkState::Pending);
977
978        // Simulate packet packing: build a fake hashable part
979        // In real usage, the caller packs a LINKREQUEST packet and calls set_link_id_from_hashable
980        let mut hashable = Vec::new();
981        hashable.push(0x00); // flags byte (lower nibble)
982        hashable.push(0x00); // hops
983        hashable.extend_from_slice(&dest_hash);
984        hashable.push(0x00); // context
985        hashable.extend_from_slice(&request_data);
986
987        initiator.set_link_id_from_hashable(&hashable, request_data.len());
988        assert_ne!(initiator.link_id(), &[0u8; 16]);
989
990        // Step 2: Responder receives link request
991        let mut rng_resp = make_rng(0x20);
992        let (mut responder, lrproof_data) = LinkEngine::new_responder(
993            &dest_sig_prv,
994            &dest_sig_pub_bytes,
995            &request_data,
996            &hashable,
997            &dest_hash,
998            1,
999            100.5,
1000            &mut rng_resp,
1001        )
1002        .unwrap();
1003        assert_eq!(responder.state(), LinkState::Handshake);
1004        assert_eq!(responder.link_id(), initiator.link_id());
1005
1006        // Step 3: Initiator validates LRPROOF
1007        let mut rng_lrrtt = make_rng(0x30);
1008        assert!(initiator
1009            .handle_lrproof_with_hops(
1010                &[0xAA; 3],
1011                &dest_sig_pub_bytes,
1012                Some(8),
1013                100.6,
1014                &mut rng_lrrtt,
1015            )
1016            .is_err());
1017        assert_eq!(initiator.expected_hops(), 1);
1018        let mismatched_mode_proof = handshake::build_lrproof(
1019            initiator.link_id(),
1020            lrproof_data[64..96].try_into().unwrap(),
1021            &dest_sig_pub_bytes,
1022            &dest_sig_prv,
1023            Some(500),
1024            LinkMode::Aes128Cbc,
1025        );
1026        assert_eq!(
1027            initiator
1028                .handle_lrproof_with_hops(
1029                    &mismatched_mode_proof,
1030                    &dest_sig_pub_bytes,
1031                    Some(9),
1032                    100.65,
1033                    &mut rng_lrrtt,
1034                )
1035                .unwrap_err(),
1036            LinkError::UnsupportedMode,
1037        );
1038        assert_eq!(initiator.expected_hops(), 1);
1039        let mut invalid_lrproof = lrproof_data.clone();
1040        invalid_lrproof[0] ^= 0x01;
1041        assert!(initiator
1042            .handle_lrproof_with_hops(
1043                &invalid_lrproof,
1044                &dest_sig_pub_bytes,
1045                Some(7),
1046                100.7,
1047                &mut rng_lrrtt,
1048            )
1049            .is_err());
1050        assert_eq!(initiator.expected_hops(), 1);
1051        let (lrrtt_encrypted, actions) = initiator
1052            .handle_lrproof_with_hops(
1053                &lrproof_data,
1054                &dest_sig_pub_bytes,
1055                Some(4),
1056                100.8,
1057                &mut rng_lrrtt,
1058            )
1059            .unwrap();
1060        assert_eq!(initiator.state(), LinkState::Active);
1061        assert!(initiator.rtt().is_some());
1062        assert_eq!(initiator.rebalanced_at(), Some(100.8));
1063        assert_eq!(actions.len(), 2); // StateChanged + LinkEstablished
1064
1065        // Step 4: Responder handles LRRTT
1066        let actions = responder
1067            .handle_lrrtt_with_hops(&lrrtt_encrypted, Some(4), 101.0)
1068            .unwrap();
1069        assert_eq!(responder.state(), LinkState::Active);
1070        assert_eq!(initiator.expected_hops(), 4);
1071        assert_eq!(responder.expected_hops(), 4);
1072        assert_eq!(responder.rebalanced_at(), None);
1073
1074        // A later valid proof cannot rewrite the first authenticated
1075        // rebalance, even if it reports another hop metric.
1076        initiator.state = LinkState::Pending;
1077        let mut repeated_rng = make_rng(0x31);
1078        initiator
1079            .handle_lrproof_with_hops(
1080                &lrproof_data,
1081                &dest_sig_pub_bytes,
1082                Some(6),
1083                101.1,
1084                &mut repeated_rng,
1085            )
1086            .unwrap();
1087        assert_eq!(initiator.expected_hops(), 4);
1088        assert_eq!(initiator.rebalanced_at(), Some(100.8));
1089
1090        initiator.record_outbound_traffic(48);
1091        initiator.record_inbound_traffic(32);
1092        assert_eq!((initiator.tx_packets(), initiator.tx_bytes()), (1, 48));
1093        assert_eq!((initiator.rx_packets(), initiator.rx_bytes()), (1, 32));
1094        assert!(responder.rtt().is_some());
1095        assert_eq!(actions.len(), 2);
1096    }
1097
1098    #[test]
1099    fn test_encrypt_decrypt_after_handshake() {
1100        let mut rng_id = make_rng(0x01);
1101        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1102        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1103        let dest_hash = [0xDD; 16];
1104
1105        let mut rng_init = make_rng(0x10);
1106        let (mut initiator, request_data) = LinkEngine::new_initiator(
1107            &dest_hash,
1108            1,
1109            LinkMode::Aes256Cbc,
1110            Some(500),
1111            100.0,
1112            &mut rng_init,
1113        );
1114        let mut hashable = Vec::new();
1115        hashable.push(0x00);
1116        hashable.push(0x00);
1117        hashable.extend_from_slice(&dest_hash);
1118        hashable.push(0x00);
1119        hashable.extend_from_slice(&request_data);
1120        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1121
1122        let mut rng_resp = make_rng(0x20);
1123        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1124            &dest_sig_prv,
1125            &dest_sig_pub_bytes,
1126            &request_data,
1127            &hashable,
1128            &dest_hash,
1129            1,
1130            100.5,
1131            &mut rng_resp,
1132        )
1133        .unwrap();
1134
1135        let mut rng_lrrtt = make_rng(0x30);
1136        let (lrrtt_encrypted, _) = initiator
1137            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1138            .unwrap();
1139        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1140
1141        // Now both sides are ACTIVE — test encrypt/decrypt
1142        let mut rng_enc = make_rng(0x40);
1143        let plaintext = b"Hello over encrypted link!";
1144        let ciphertext = initiator.encrypt(plaintext, &mut rng_enc).unwrap();
1145        let decrypted = responder.decrypt(&ciphertext).unwrap();
1146        assert_eq!(decrypted, plaintext);
1147
1148        // And in reverse
1149        let mut rng_enc2 = make_rng(0x50);
1150        let ciphertext2 = responder.encrypt(b"Reply!", &mut rng_enc2).unwrap();
1151        let decrypted2 = initiator.decrypt(&ciphertext2).unwrap();
1152        assert_eq!(decrypted2, b"Reply!");
1153    }
1154
1155    #[test]
1156    fn test_tick_establishment_timeout() {
1157        let mut rng = make_rng(0x10);
1158        let dest_hash = [0xDD; 16];
1159        let (mut engine, _) =
1160            LinkEngine::new_initiator(&dest_hash, 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1161        // Timeout = 6.0 + 6.0 * 1 = 12.0s → expires at 112.0
1162
1163        // Before timeout — no state change
1164        let actions = engine.tick(110.0);
1165        assert!(actions.is_empty());
1166
1167        // After timeout
1168        let actions = engine.tick(113.0);
1169        assert_eq!(actions.len(), 1);
1170        assert_eq!(engine.state(), LinkState::Closed);
1171    }
1172
1173    #[test]
1174    fn test_tick_stale_and_close() {
1175        let mut rng_id = make_rng(0x01);
1176        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1177        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1178        let dest_hash = [0xDD; 16];
1179
1180        let mut rng_init = make_rng(0x10);
1181        let (mut initiator, request_data) = LinkEngine::new_initiator(
1182            &dest_hash,
1183            1,
1184            LinkMode::Aes256Cbc,
1185            Some(500),
1186            100.0,
1187            &mut rng_init,
1188        );
1189        let mut hashable = Vec::new();
1190        hashable.push(0x00);
1191        hashable.push(0x00);
1192        hashable.extend_from_slice(&dest_hash);
1193        hashable.push(0x00);
1194        hashable.extend_from_slice(&request_data);
1195        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1196
1197        let mut rng_resp = make_rng(0x20);
1198        let (_, lrproof_data) = LinkEngine::new_responder(
1199            &dest_sig_prv,
1200            &dest_sig_pub_bytes,
1201            &request_data,
1202            &hashable,
1203            &dest_hash,
1204            1,
1205            100.5,
1206            &mut rng_resp,
1207        )
1208        .unwrap();
1209
1210        let mut rng_lrrtt = make_rng(0x30);
1211        initiator
1212            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1213            .unwrap();
1214        assert_eq!(initiator.state(), LinkState::Active);
1215
1216        // Advance time past stale_time
1217        let stale_time = initiator.stale_time;
1218        let went_stale = 100.8 + stale_time + 1.0;
1219        let actions = initiator.tick(went_stale);
1220        assert_eq!(initiator.state(), LinkState::Stale);
1221        assert_eq!(actions.len(), 1);
1222
1223        // STALE holds for `rtt * KEEPALIVE_TIMEOUT_FACTOR + STALE_GRACE`
1224        // before closing, like the Python watchdog's stale sleep.
1225        let grace = stale_close_timeout(initiator.rtt().unwrap());
1226        assert!(grace >= LINK_STALE_GRACE);
1227        let actions = initiator.tick(went_stale + grace - 0.1);
1228        assert_eq!(initiator.state(), LinkState::Stale);
1229        assert!(actions.is_empty());
1230
1231        let actions = initiator.tick(went_stale + grace);
1232        assert_eq!(initiator.state(), LinkState::Closed);
1233        assert_eq!(actions.len(), 1);
1234        assert!(matches!(
1235            actions[0],
1236            LinkAction::StateChanged {
1237                new_state: LinkState::Closed,
1238                reason: Some(TeardownReason::Timeout),
1239                ..
1240            }
1241        ));
1242    }
1243
1244    #[test]
1245    fn inbound_during_stale_grace_reactivates_the_link() {
1246        let (mut initiator, _, _) = active_link_pair();
1247        let stale_time = initiator.stale_time;
1248        let went_stale = 101.0 + stale_time + 1.0;
1249        initiator.tick(went_stale);
1250        assert_eq!(initiator.state(), LinkState::Stale);
1251
1252        let actions = initiator.record_inbound(went_stale + 1.0);
1253        assert_eq!(initiator.state(), LinkState::Active);
1254        assert_eq!(actions.len(), 1);
1255
1256        // The recovered link only goes stale again after a full quiet period.
1257        let grace = stale_close_timeout(initiator.rtt().unwrap());
1258        assert!(initiator.tick(went_stale + grace + 1.0).is_empty());
1259        assert_eq!(initiator.state(), LinkState::Active);
1260        initiator.tick(went_stale + 1.0 + stale_time);
1261        assert_eq!(initiator.state(), LinkState::Stale);
1262    }
1263
1264    #[test]
1265    fn received_proofs_keep_an_initiator_from_going_stale() {
1266        let (mut initiator, _, _) = active_link_pair();
1267        let stale_time = initiator.stale_time;
1268        // Only proofs come back for a long stream of confirmed sends.
1269        initiator.record_proof(101.0 + stale_time - 1.0);
1270        assert!(initiator.tick(101.0 + stale_time + 1.0).is_empty());
1271        assert_eq!(initiator.state(), LinkState::Active);
1272
1273        // A stale link is not reactivated by a proof alone.
1274        initiator.tick(101.0 + 2.0 * stale_time);
1275        assert_eq!(initiator.state(), LinkState::Stale);
1276        initiator.record_proof(101.0 + 2.0 * stale_time + 1.0);
1277        assert_eq!(initiator.state(), LinkState::Stale);
1278    }
1279
1280    #[test]
1281    fn test_needs_keepalive() {
1282        let mut rng_id = make_rng(0x01);
1283        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1284        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1285        let dest_hash = [0xDD; 16];
1286
1287        let mut rng_init = make_rng(0x10);
1288        let (mut initiator, request_data) = LinkEngine::new_initiator(
1289            &dest_hash,
1290            1,
1291            LinkMode::Aes256Cbc,
1292            Some(500),
1293            100.0,
1294            &mut rng_init,
1295        );
1296        let mut hashable = Vec::new();
1297        hashable.push(0x00);
1298        hashable.push(0x00);
1299        hashable.extend_from_slice(&dest_hash);
1300        hashable.push(0x00);
1301        hashable.extend_from_slice(&request_data);
1302        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1303
1304        let mut rng_resp = make_rng(0x20);
1305        let (_, lrproof_data) = LinkEngine::new_responder(
1306            &dest_sig_prv,
1307            &dest_sig_pub_bytes,
1308            &request_data,
1309            &hashable,
1310            &dest_hash,
1311            1,
1312            100.5,
1313            &mut rng_resp,
1314        )
1315        .unwrap();
1316
1317        let mut rng_lrrtt = make_rng(0x30);
1318        initiator
1319            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1320            .unwrap();
1321
1322        let ka = initiator.keepalive_interval();
1323        // Not yet
1324        assert!(!initiator.needs_keepalive(100.8 + ka - 1.0));
1325        // Past keepalive
1326        assert!(initiator.needs_keepalive(100.8 + ka + 1.0));
1327    }
1328
1329    #[test]
1330    fn initiator_probes_when_outbound_is_quiet_despite_recent_inbound() {
1331        let (mut initiator, _, _) = active_link_pair();
1332        let keepalive = initiator.keepalive_interval();
1333        let now = 1_000.0;
1334
1335        initiator.record_inbound(now - 0.1);
1336        initiator.record_outbound(now - keepalive - 0.1, false);
1337
1338        assert!(initiator.needs_keepalive(now));
1339    }
1340
1341    #[test]
1342    fn initiator_probes_when_inbound_is_quiet_despite_recent_outbound() {
1343        let (mut initiator, _, _) = active_link_pair();
1344        let keepalive = initiator.keepalive_interval();
1345        let now = 1_000.0;
1346
1347        initiator.record_inbound(now - keepalive - 0.1);
1348        initiator.record_outbound(now - 0.1, false);
1349
1350        assert!(initiator.needs_keepalive(now));
1351    }
1352
1353    #[test]
1354    fn initiator_does_not_probe_when_both_directions_are_recent() {
1355        let (mut initiator, _, _) = active_link_pair();
1356        let now = 1_000.0;
1357
1358        initiator.record_inbound(now - 0.1);
1359        initiator.record_outbound(now - 0.1, false);
1360
1361        assert!(!initiator.needs_keepalive(now));
1362    }
1363
1364    #[test]
1365    fn test_needs_keepalive_responder() {
1366        let mut rng_id = make_rng(0x01);
1367        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1368        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1369        let dest_hash = [0xDD; 16];
1370
1371        let mut rng_init = make_rng(0x10);
1372        let (mut initiator, request_data) = LinkEngine::new_initiator(
1373            &dest_hash,
1374            1,
1375            LinkMode::Aes256Cbc,
1376            Some(500),
1377            100.0,
1378            &mut rng_init,
1379        );
1380        let mut hashable = Vec::new();
1381        hashable.push(0x00);
1382        hashable.push(0x00);
1383        hashable.extend_from_slice(&dest_hash);
1384        hashable.push(0x00);
1385        hashable.extend_from_slice(&request_data);
1386        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1387
1388        let mut rng_resp = make_rng(0x20);
1389        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1390            &dest_sig_prv,
1391            &dest_sig_pub_bytes,
1392            &request_data,
1393            &hashable,
1394            &dest_hash,
1395            1,
1396            100.5,
1397            &mut rng_resp,
1398        )
1399        .unwrap();
1400
1401        let mut rng_lrrtt = make_rng(0x30);
1402        let (lrrtt_encrypted, _) = initiator
1403            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1404            .unwrap();
1405        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1406
1407        let ka = responder.keepalive_interval();
1408        // Only the initiator emits probes. The responder replies to probes.
1409        assert!(!responder.needs_keepalive(101.0 + ka - 1.0));
1410        assert!(!responder.needs_keepalive(101.0 + ka + 1.0));
1411    }
1412
1413    #[test]
1414    fn responder_replies_only_to_probe_and_only_when_outbound_is_quiet() {
1415        let (_, mut responder, _) = active_link_pair();
1416        let keepalive = responder.keepalive_interval();
1417        let now = 1_000.0;
1418
1419        responder.record_outbound(now - keepalive - 0.1, false);
1420        assert!(responder.should_reply_keepalive(&[0xff], now));
1421        assert!(!responder.should_reply_keepalive(&[0xfe], now));
1422        assert!(!responder.should_reply_keepalive(&[], now));
1423
1424        responder.record_outbound(now - 0.1, false);
1425        assert!(!responder.should_reply_keepalive(&[0xff], now));
1426    }
1427
1428    #[test]
1429    fn initiator_never_replies_to_keepalive_probe() {
1430        let (mut initiator, _, _) = active_link_pair();
1431        let keepalive = initiator.keepalive_interval();
1432        let now = 1_000.0;
1433        initiator.record_outbound(now - keepalive - 0.1, false);
1434
1435        assert!(!initiator.should_reply_keepalive(&[0xff], now));
1436    }
1437
1438    #[test]
1439    fn packet_delivery_proofs_are_bidirectional_and_peer_authenticated() {
1440        let (initiator, responder, _) = active_link_pair();
1441        let packet_hash = [0x42; 32];
1442
1443        let initiator_proof = initiator.sign_packet_hash(&packet_hash);
1444        assert!(responder.validate_packet_proof(&packet_hash, &initiator_proof));
1445        assert!(!initiator.validate_packet_proof(&packet_hash, &initiator_proof));
1446
1447        let responder_proof = responder.sign_packet_hash(&packet_hash);
1448        assert!(initiator.validate_packet_proof(&packet_hash, &responder_proof));
1449        assert!(!responder.validate_packet_proof(&packet_hash, &responder_proof));
1450        assert!(!initiator.validate_packet_proof(&[0x43; 32], &responder_proof));
1451    }
1452
1453    #[test]
1454    fn test_teardown() {
1455        let mut rng = make_rng(0x10);
1456        let (mut engine, _) =
1457            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1458        let actions = engine.teardown();
1459        assert_eq!(engine.state(), LinkState::Closed);
1460        assert_eq!(actions.len(), 1);
1461
1462        // Teardown again is no-op
1463        let actions = engine.teardown();
1464        assert!(actions.is_empty());
1465    }
1466
1467    #[test]
1468    fn test_handle_teardown() {
1469        let mut rng = make_rng(0x10);
1470        let (mut engine, _) =
1471            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1472        let actions = engine.handle_teardown();
1473        assert_eq!(engine.state(), LinkState::Closed);
1474        assert_eq!(actions.len(), 1);
1475        match &actions[0] {
1476            LinkAction::StateChanged { reason, .. } => {
1477                assert_eq!(*reason, Some(TeardownReason::DestinationClosed));
1478            }
1479            _ => panic!("Expected StateChanged"),
1480        }
1481    }
1482
1483    #[test]
1484    fn test_identify_over_link() {
1485        let mut rng_id = make_rng(0x01);
1486        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1487        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1488        let dest_hash = [0xDD; 16];
1489
1490        let mut rng_init = make_rng(0x10);
1491        let (mut initiator, request_data) = LinkEngine::new_initiator(
1492            &dest_hash,
1493            1,
1494            LinkMode::Aes256Cbc,
1495            Some(500),
1496            100.0,
1497            &mut rng_init,
1498        );
1499        let mut hashable = Vec::new();
1500        hashable.push(0x00);
1501        hashable.push(0x00);
1502        hashable.extend_from_slice(&dest_hash);
1503        hashable.push(0x00);
1504        hashable.extend_from_slice(&request_data);
1505        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1506
1507        let mut rng_resp = make_rng(0x20);
1508        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1509            &dest_sig_prv,
1510            &dest_sig_pub_bytes,
1511            &request_data,
1512            &hashable,
1513            &dest_hash,
1514            1,
1515            100.5,
1516            &mut rng_resp,
1517        )
1518        .unwrap();
1519
1520        let mut rng_lrrtt = make_rng(0x30);
1521        let (lrrtt_encrypted, _) = initiator
1522            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1523            .unwrap();
1524        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1525
1526        // Create identity to identify with
1527        let mut rng_ident = make_rng(0x40);
1528        let my_identity = rns_crypto::identity::Identity::new(&mut rng_ident);
1529
1530        // Initiator identifies itself to responder
1531        let mut rng_enc = make_rng(0x50);
1532        let identify_encrypted = initiator
1533            .build_identify(&my_identity, &mut rng_enc)
1534            .unwrap();
1535
1536        let actions = responder.handle_identify(&identify_encrypted).unwrap();
1537        assert_eq!(actions.len(), 1);
1538        match &actions[0] {
1539            LinkAction::RemoteIdentified {
1540                identity_hash,
1541                public_key,
1542                ..
1543            } => {
1544                assert_eq!(identity_hash, my_identity.hash());
1545                assert_eq!(public_key, &my_identity.get_public_key().unwrap());
1546            }
1547            _ => panic!("Expected RemoteIdentified"),
1548        }
1549
1550        // Valid repeated identifies are authenticated, but identification is
1551        // one-shot and cannot replace the first identity or emit callbacks.
1552        let mut rng_repeat = make_rng(0x51);
1553        let repeated = initiator
1554            .build_identify(&my_identity, &mut rng_repeat)
1555            .unwrap();
1556        assert!(responder.handle_identify(&repeated).unwrap().is_empty());
1557
1558        let mut rng_other = make_rng(0x60);
1559        let other_identity = rns_crypto::identity::Identity::new(&mut rng_other);
1560        let mut rng_other_enc = make_rng(0x61);
1561        let other = initiator
1562            .build_identify(&other_identity, &mut rng_other_enc)
1563            .unwrap();
1564        assert!(responder.handle_identify(&other).unwrap().is_empty());
1565        assert_eq!(
1566            responder.remote_identity().map(|(hash, _)| hash),
1567            Some(my_identity.hash())
1568        );
1569
1570        // Malformed repeats are still rejected cryptographically.
1571        let mut rng_bad = make_rng(0x70);
1572        let malformed = initiator
1573            .encrypt(b"invalid identify", &mut rng_bad)
1574            .unwrap();
1575        assert!(responder.handle_identify(&malformed).is_err());
1576        assert_eq!(
1577            responder.remote_identity().map(|(hash, _)| hash),
1578            Some(my_identity.hash())
1579        );
1580    }
1581
1582    #[test]
1583    fn test_aes128_mode_handshake() {
1584        let mut rng_id = make_rng(0x01);
1585        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1586        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1587        let dest_hash = [0xDD; 16];
1588
1589        let mut rng_init = make_rng(0x10);
1590        let (mut initiator, request_data) = LinkEngine::new_initiator(
1591            &dest_hash,
1592            1,
1593            LinkMode::Aes128Cbc,
1594            Some(500),
1595            100.0,
1596            &mut rng_init,
1597        );
1598        let mut hashable = Vec::new();
1599        hashable.push(0x00);
1600        hashable.push(0x00);
1601        hashable.extend_from_slice(&dest_hash);
1602        hashable.push(0x00);
1603        hashable.extend_from_slice(&request_data);
1604        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1605
1606        let mut rng_resp = make_rng(0x20);
1607        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1608            &dest_sig_prv,
1609            &dest_sig_pub_bytes,
1610            &request_data,
1611            &hashable,
1612            &dest_hash,
1613            1,
1614            100.5,
1615            &mut rng_resp,
1616        )
1617        .unwrap();
1618
1619        let mut rng_lrrtt = make_rng(0x30);
1620        let (lrrtt_encrypted, _) = initiator
1621            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1622            .unwrap();
1623        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1624
1625        assert_eq!(initiator.state(), LinkState::Active);
1626        assert_eq!(responder.state(), LinkState::Active);
1627        assert_eq!(initiator.mode(), LinkMode::Aes128Cbc);
1628
1629        // Verify encrypt/decrypt works
1630        let mut rng_enc = make_rng(0x40);
1631        let ct = initiator.encrypt(b"AES128 test", &mut rng_enc).unwrap();
1632        let pt = responder.decrypt(&ct).unwrap();
1633        assert_eq!(pt, b"AES128 test");
1634    }
1635
1636    /// Run LINKREQUEST + LRPROOF with the given signalled MTU. Returns the
1637    /// active initiator and the responder still waiting for its LRRTT.
1638    fn handshake_pair_with_mtu(mtu: Option<u32>) -> (LinkEngine, LinkEngine) {
1639        let mut rng_id = make_rng(0x01);
1640        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1641        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1642        let dest_hash = [0xDD; 16];
1643
1644        let mut rng_init = make_rng(0x10);
1645        let (mut initiator, request_data) = LinkEngine::new_initiator(
1646            &dest_hash,
1647            1,
1648            LinkMode::Aes256Cbc,
1649            mtu,
1650            100.0,
1651            &mut rng_init,
1652        );
1653        let mut hashable = vec![0x00, 0x00];
1654        hashable.extend_from_slice(&dest_hash);
1655        hashable.push(0x00);
1656        hashable.extend_from_slice(&request_data);
1657        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1658
1659        let mut rng_resp = make_rng(0x20);
1660        let (responder, lrproof_data) = LinkEngine::new_responder(
1661            &dest_sig_prv,
1662            &dest_sig_pub_bytes,
1663            &request_data,
1664            &hashable,
1665            &dest_hash,
1666            1,
1667            100.5,
1668            &mut rng_resp,
1669        )
1670        .unwrap();
1671
1672        let mut rng_lrrtt = make_rng(0x30);
1673        initiator
1674            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1675            .unwrap();
1676        (initiator, responder)
1677    }
1678
1679    fn raw_rtt_payload(rtt: f64) -> Vec<u8> {
1680        let mut data = vec![0xcb];
1681        data.extend_from_slice(&rtt.to_be_bytes());
1682        data
1683    }
1684
1685    #[test]
1686    fn compute_mdu_keeps_values_for_regular_mtus() {
1687        assert_eq!(compute_mdu(500), 431);
1688        assert_eq!(compute_mdu(508), 431);
1689        assert_eq!(compute_mdu(1064), 991);
1690        assert_eq!(compute_mdu(8192), 8111);
1691        assert_eq!(compute_mdu(262_144), 262_063);
1692        assert_eq!(compute_mdu(LINK_MTU_MAX as usize), 2_097_071);
1693    }
1694
1695    #[test]
1696    fn compute_mdu_saturates_for_small_mtus() {
1697        for mtu in [0usize, 1, 50, 67, 68, 83] {
1698            assert_eq!(compute_mdu(mtu), 0, "mtu {mtu}");
1699        }
1700        assert_eq!(compute_mdu(LINK_MTU_MIN as usize), 15);
1701    }
1702
1703    #[test]
1704    fn effective_link_mtu_is_clamped_and_mdu_monotone() {
1705        assert_eq!(effective_link_mtu(None), MTU as u32);
1706        assert_eq!(effective_link_mtu(Some(0)), MTU as u32);
1707        assert_eq!(effective_link_mtu(Some(1)), LINK_MTU_MIN);
1708        assert_eq!(effective_link_mtu(Some(50)), LINK_MTU_MIN);
1709        assert_eq!(effective_link_mtu(Some(83)), LINK_MTU_MIN);
1710        assert_eq!(effective_link_mtu(Some(84)), 84);
1711        assert_eq!(effective_link_mtu(Some(500)), 500);
1712        assert_eq!(effective_link_mtu(Some(LINK_MTU_MAX)), LINK_MTU_MAX);
1713        assert_eq!(effective_link_mtu(Some(u32::MAX)), LINK_MTU_MAX);
1714
1715        let mut prev = 0usize;
1716        for mtu in [1u32, 50, 83, 84, 500, LINK_MTU_MAX] {
1717            let mdu = compute_mdu(effective_link_mtu(Some(mtu)) as usize);
1718            assert!(mdu >= 15, "mtu {mtu} gave mdu {mdu}");
1719            assert!(mdu >= prev, "mdu not monotone at mtu {mtu}");
1720            prev = mdu;
1721        }
1722    }
1723
1724    #[test]
1725    fn small_signalled_mtu_is_raised_to_minimum_on_both_sides() {
1726        let (initiator, mut responder) = handshake_pair_with_mtu(Some(70));
1727        assert_eq!(responder.mtu(), LINK_MTU_MIN);
1728        assert_eq!(responder.mdu(), 15);
1729        assert_eq!(initiator.mtu(), LINK_MTU_MIN);
1730        assert_eq!(initiator.mdu(), 15);
1731        assert_eq!(initiator.state(), LinkState::Active);
1732
1733        let mut rng = make_rng(0x40);
1734        let lrrtt = initiator.encrypt(&pack_rtt(0.3), &mut rng).unwrap();
1735        responder.handle_lrrtt(&lrrtt, 101.0).unwrap();
1736        assert_eq!(responder.state(), LinkState::Active);
1737
1738        let ct = initiator.encrypt(b"hello", &mut rng).unwrap();
1739        assert_eq!(responder.decrypt(&ct).unwrap(), b"hello");
1740    }
1741
1742    #[test]
1743    fn signalled_mtu_edge_values_do_not_panic() {
1744        for mtu in [0u32, 1, 50, 83, 84, 500, LINK_MTU_MAX] {
1745            let (initiator, responder) = handshake_pair_with_mtu(Some(mtu));
1746            assert!(responder.mtu() >= LINK_MTU_MIN);
1747            assert!(responder.mdu() >= 15);
1748            assert_eq!(initiator.mtu(), responder.mtu());
1749            assert_eq!(initiator.mdu(), responder.mdu());
1750        }
1751    }
1752
1753    #[test]
1754    fn non_finite_or_negative_lrrtt_is_rejected() {
1755        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0] {
1756            let (initiator, mut responder) = handshake_pair_with_mtu(Some(500));
1757            let mut rng = make_rng(0x40);
1758            let lrrtt = initiator.encrypt(&raw_rtt_payload(bad), &mut rng).unwrap();
1759            assert!(
1760                matches!(
1761                    responder.handle_lrrtt(&lrrtt, 101.0),
1762                    Err(LinkError::InvalidData)
1763                ),
1764                "rtt {bad}"
1765            );
1766            assert_eq!(responder.state(), LinkState::Handshake);
1767            assert_eq!(responder.rtt(), None);
1768            assert!(responder.keepalive_interval.is_finite());
1769            assert!(responder.stale_time.is_finite());
1770
1771            // The peer goes silent: the pending link still times out.
1772            let deadline = responder.request_time + responder.establishment_timeout;
1773            let actions = responder.tick(deadline + 1.0);
1774            assert_eq!(responder.state(), LinkState::Closed);
1775            assert_eq!(actions.len(), 1);
1776        }
1777    }
1778
1779    #[test]
1780    fn valid_lrrtt_updates_rtt_and_timers() {
1781        let (initiator, mut responder) = handshake_pair_with_mtu(Some(500));
1782        let mut rng = make_rng(0x40);
1783        // Larger than the responder's own measurement (0.5s), so it is selected.
1784        let lrrtt = initiator.encrypt(&pack_rtt(0.9), &mut rng).unwrap();
1785        responder.handle_lrrtt(&lrrtt, 101.0).unwrap();
1786        assert_eq!(responder.state(), LinkState::Active);
1787        assert_eq!(responder.rtt(), Some(0.9));
1788        assert_eq!(responder.keepalive_interval, compute_keepalive(0.9));
1789        assert_eq!(
1790            responder.stale_time,
1791            compute_stale_time(compute_keepalive(0.9))
1792        );
1793    }
1794
1795    #[test]
1796    fn non_finite_rtt_keeps_timers_finite_and_link_goes_stale() {
1797        let (_, mut responder, _) = active_link_pair();
1798        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1799            responder.set_rtt(bad);
1800            assert!(responder.keepalive_interval.is_finite(), "rtt {bad}");
1801            assert!(responder.stale_time.is_finite(), "rtt {bad}");
1802            assert!(responder.stale_time <= LINK_KEEPALIVE_MAX * 2.0);
1803        }
1804
1805        let stale_time = responder.stale_time;
1806        responder.tick(101.0 + stale_time + 1.0);
1807        assert_eq!(responder.state(), LinkState::Stale);
1808    }
1809
1810    #[test]
1811    fn stamped_link_request_carries_required_value() {
1812        let mut rng = rns_crypto::OsRng;
1813        let dest_hash = [0xAB; 16];
1814        let (_engine, data) = LinkEngine::new_initiator_stamped(
1815            &dest_hash,
1816            1,
1817            LinkMode::Aes256Cbc,
1818            None,
1819            0.0,
1820            &mut rng,
1821            crate::stamp::StampCost::from_whole(4),
1822        );
1823        let value = handshake::link_request_stamp_value(&dest_hash, &data).unwrap();
1824        assert!(value.meets(crate::stamp::StampCost::from_whole(4)));
1825    }
1826
1827    #[test]
1828    fn initiator_with_keys_uses_the_supplied_keys() {
1829        let mut rng = rns_crypto::OsRng;
1830        let dest_hash = [0xAC; 16];
1831        let prv = X25519PrivateKey::generate(&mut rng);
1832        let sig_prv = Ed25519PrivateKey::generate(&mut rng);
1833        let expected_pub = prv.public_key().public_bytes();
1834        let expected_sig_pub = sig_prv.public_key().public_bytes();
1835
1836        let (engine, data) = LinkEngine::new_initiator_with_keys(
1837            &dest_hash,
1838            1,
1839            LinkMode::Aes256Cbc,
1840            None,
1841            0.0,
1842            prv,
1843            sig_prv,
1844        );
1845        let (peer_pub, peer_sig_pub, mtu, mode) = handshake::parse_linkrequest_data(&data).unwrap();
1846        assert_eq!(peer_pub, expected_pub);
1847        assert_eq!(peer_sig_pub, expected_sig_pub);
1848        assert_eq!(mtu, None);
1849        assert_eq!(mode, LinkMode::Aes256Cbc);
1850        assert!(engine.is_initiator());
1851        assert_eq!(engine.state(), LinkState::Pending);
1852    }
1853}