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