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,
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    /// Decrypt ciphertext held in consecutive chunks without joining them,
394    /// dropping the first `skip` plaintext bytes.
395    pub fn decrypt_chunks(&self, chunks: &[&[u8]], skip: usize) -> Result<Vec<u8>, LinkError> {
396        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
397        link_decrypt_chunks(token, chunks, skip)
398    }
399
400    /// Sign an explicit delivery proof for a packet sent over this Link.
401    pub fn sign_packet_hash(&self, packet_hash: &[u8; 32]) -> [u8; 64] {
402        self.sig_prv.sign(packet_hash)
403    }
404
405    /// Validate a delivery proof using the remote endpoint's Link signing key.
406    pub fn validate_packet_proof(&self, packet_hash: &[u8; 32], signature: &[u8; 64]) -> bool {
407        self.peer_sig_pub_bytes
408            .and_then(|bytes| Ed25519PublicKey::from_bytes(&bytes).ok())
409            .map(|key| key.verify(signature, packet_hash))
410            .unwrap_or(false)
411    }
412
413    /// Build LINKIDENTIFY data (encrypted).
414    pub fn build_identify(
415        &self,
416        identity: &rns_crypto::identity::Identity,
417        rng: &mut dyn Rng,
418    ) -> Result<Vec<u8>, LinkError> {
419        if self.state != LinkState::Active {
420            return Err(LinkError::InvalidState);
421        }
422        let plaintext = identify::build_identify_data(identity, &self.link_id)?;
423        self.encrypt(&plaintext, rng)
424    }
425
426    /// Handle incoming LINKIDENTIFY (encrypted data).
427    ///
428    /// Only responders (non-initiators) can receive LINKIDENTIFY (Python: Link.py:1017).
429    pub fn handle_identify(&mut self, encrypted_data: &[u8]) -> Result<Vec<LinkAction>, LinkError> {
430        if self.state != LinkState::Active || self.is_initiator {
431            return Err(LinkError::InvalidState);
432        }
433
434        let plaintext = self.decrypt(encrypted_data)?;
435        let (identity_hash, public_key) =
436            identify::validate_identify_data(&plaintext, &self.link_id)?;
437        if self.remote_identity.is_some() {
438            return Ok(Vec::new());
439        }
440        self.remote_identity = Some((identity_hash, public_key));
441
442        Ok(alloc::vec![LinkAction::RemoteIdentified {
443            link_id: self.link_id,
444            identity_hash,
445            public_key,
446        }])
447    }
448
449    /// Record that an inbound packet was received (updates timing).
450    ///
451    /// If the link is STALE, recovers to ACTIVE (Python: Link.py:987-988).
452    pub fn record_inbound(&mut self, now: f64) -> Vec<LinkAction> {
453        self.last_inbound = now;
454        if self.state == LinkState::Stale {
455            self.state = LinkState::Active;
456            return alloc::vec![LinkAction::StateChanged {
457                link_id: self.link_id,
458                new_state: LinkState::Active,
459                reason: None,
460            }];
461        }
462        Vec::new()
463    }
464
465    /// Record that a proof was received (updates timing for stale detection).
466    pub fn record_proof(&mut self, now: f64) {
467        self.last_proof = now;
468    }
469
470    /// Record that an outbound packet was sent (updates timing).
471    pub fn record_outbound(&mut self, now: f64, is_keepalive: bool) {
472        self.last_outbound = now;
473        if is_keepalive {
474            self.last_keepalive = now;
475        }
476    }
477
478    /// Record one accepted inbound packet using its packed data/ciphertext size.
479    pub fn record_inbound_traffic(&mut self, data_len: usize) {
480        if self.state != LinkState::Closed {
481            self.rx_packets = self.rx_packets.saturating_add(1);
482            self.rx_bytes = self.rx_bytes.saturating_add(data_len as u64);
483        }
484    }
485
486    /// Record one successfully packed outbound packet.
487    pub fn record_outbound_traffic(&mut self, data_len: usize) {
488        self.tx_packets = self.tx_packets.saturating_add(1);
489        self.tx_bytes = self.tx_bytes.saturating_add(data_len as u64);
490    }
491
492    /// Periodic tick: check keepalive, stale, timeouts.
493    pub fn tick(&mut self, now: f64) -> Vec<LinkAction> {
494        let mut actions = Vec::new();
495
496        match self.state {
497            LinkState::Pending | LinkState::Handshake => {
498                if is_establishment_timeout(self.request_time, self.establishment_timeout, now) {
499                    self.state = LinkState::Closed;
500                    actions.push(LinkAction::StateChanged {
501                        link_id: self.link_id,
502                        new_state: LinkState::Closed,
503                        reason: Some(TeardownReason::Timeout),
504                    });
505                }
506            }
507            LinkState::Active => {
508                let activated = self.activated_at.unwrap_or(0.0);
509                // Python: max(max(self.last_inbound, self.last_proof), activated_at)
510                let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
511
512                if should_go_stale(last_inbound, self.stale_time, now) {
513                    self.state = LinkState::Stale;
514                    actions.push(LinkAction::StateChanged {
515                        link_id: self.link_id,
516                        new_state: LinkState::Stale,
517                        reason: None,
518                    });
519                }
520            }
521            LinkState::Stale => {
522                // In Python, STALE immediately sends teardown and closes
523                self.state = LinkState::Closed;
524                actions.push(LinkAction::StateChanged {
525                    link_id: self.link_id,
526                    new_state: LinkState::Closed,
527                    reason: Some(TeardownReason::Timeout),
528                });
529            }
530            LinkState::Closed => {}
531        }
532
533        actions
534    }
535
536    /// Check if a keepalive should be sent. Returns true if conditions are met.
537    pub fn needs_keepalive(&self, now: f64) -> bool {
538        if self.state != LinkState::Active || !self.is_initiator {
539            return false;
540        }
541        let activated = self.activated_at.unwrap_or(0.0);
542        let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
543
544        // Initiators probe whenever either direction has been quiet for a full
545        // interval. In particular, continuous responder-to-initiator traffic
546        // must not suppress the probe that keeps the responder's inbound timer
547        // alive.
548        let inbound_quiet = now >= last_inbound + self.keepalive_interval;
549        let outbound_quiet = now >= self.last_outbound + self.keepalive_interval;
550        if !inbound_quiet && !outbound_quiet {
551            return false;
552        }
553
554        should_send_keepalive(self.last_keepalive, self.keepalive_interval, now)
555    }
556
557    /// Return whether a received keepalive probe needs a responder reply.
558    ///
559    /// `0xff` is the initiator probe and `0xfe` the responder acknowledgement,
560    /// matching upstream Reticulum. A responder that sent any traffic within
561    /// the current keepalive interval can omit the acknowledgement because that
562    /// traffic already refreshed the initiator's inbound timer.
563    pub fn should_reply_keepalive(&self, payload: &[u8], now: f64) -> bool {
564        self.state == LinkState::Active
565            && !self.is_initiator
566            && payload == [0xff]
567            && now >= self.last_outbound + self.keepalive_interval
568    }
569
570    /// Tear down the link (initiator-initiated close).
571    pub fn teardown(&mut self) -> Vec<LinkAction> {
572        if self.state == LinkState::Closed {
573            return Vec::new();
574        }
575        self.state = LinkState::Closed;
576        let reason = if self.is_initiator {
577            TeardownReason::InitiatorClosed
578        } else {
579            TeardownReason::DestinationClosed
580        };
581        alloc::vec![LinkAction::StateChanged {
582            link_id: self.link_id,
583            new_state: LinkState::Closed,
584            reason: Some(reason),
585        }]
586    }
587
588    /// Handle incoming teardown (remote close).
589    pub fn handle_teardown(&mut self) -> Vec<LinkAction> {
590        if self.state == LinkState::Closed {
591            return Vec::new();
592        }
593        self.state = LinkState::Closed;
594        let reason = if self.is_initiator {
595            TeardownReason::DestinationClosed
596        } else {
597            TeardownReason::InitiatorClosed
598        };
599        alloc::vec![LinkAction::StateChanged {
600            link_id: self.link_id,
601            new_state: LinkState::Closed,
602            reason: Some(reason),
603        }]
604    }
605
606    // --- Queries ---
607
608    pub fn link_id(&self) -> &LinkId {
609        &self.link_id
610    }
611
612    pub fn state(&self) -> LinkState {
613        self.state
614    }
615
616    pub fn rtt(&self) -> Option<f64> {
617        self.rtt
618    }
619
620    pub fn mdu(&self) -> usize {
621        self.mdu
622    }
623
624    pub fn mtu(&self) -> u32 {
625        self.mtu
626    }
627
628    pub fn is_initiator(&self) -> bool {
629        self.is_initiator
630    }
631
632    pub fn mode(&self) -> LinkMode {
633        self.mode
634    }
635
636    pub fn remote_identity(&self) -> Option<&([u8; 16], [u8; 64])> {
637        self.remote_identity.as_ref()
638    }
639
640    pub fn destination_hash(&self) -> &[u8; 16] {
641        &self.destination_hash
642    }
643
644    pub fn expected_hops(&self) -> u8 {
645        self.expected_hops
646    }
647
648    pub fn rebalanced_at(&self) -> Option<f64> {
649        self.rebalanced_at
650    }
651
652    pub fn tx_packets(&self) -> u64 {
653        self.tx_packets
654    }
655
656    pub fn rx_packets(&self) -> u64 {
657        self.rx_packets
658    }
659
660    pub fn tx_bytes(&self) -> u64 {
661        self.tx_bytes
662    }
663
664    pub fn rx_bytes(&self) -> u64 {
665        self.rx_bytes
666    }
667
668    /// Get the derived session key (needed for hole-punch token derivation).
669    pub fn derived_key(&self) -> Option<&[u8]> {
670        self.derived_key.as_deref()
671    }
672
673    pub fn keepalive_interval(&self) -> f64 {
674        self.keepalive_interval
675    }
676
677    /// Update the measured RTT (e.g., after path redirect to a direct link).
678    /// Also recalculates keepalive and stale timers.
679    pub fn set_rtt(&mut self, rtt: f64) {
680        self.rtt = Some(rtt);
681        self.update_keepalive();
682    }
683
684    /// Update the link MTU (e.g., after path redirect to a different interface).
685    pub fn set_mtu(&mut self, mtu: u32) {
686        self.mtu = mtu;
687        self.mdu = compute_mdu(mtu as usize);
688    }
689
690    #[doc(hidden)]
691    pub fn clear_session_for_testing(&mut self) {
692        self.derived_key = None;
693        self.token = None;
694    }
695
696    // --- Internal ---
697
698    fn update_keepalive(&mut self) {
699        if let Some(rtt) = self.rtt {
700            self.keepalive_interval = compute_keepalive(rtt);
701            self.stale_time = compute_stale_time(self.keepalive_interval);
702        }
703    }
704}
705
706/// Compute link MDU from MTU.
707///
708/// MDU = floor((mtu - IFAC_MIN_SIZE - HEADER_MINSIZE - TOKEN_OVERHEAD) / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
709///
710/// Saturates at 0 for MTUs too small to carry one AES block.
711fn compute_mdu(mtu: usize) -> usize {
712    use crate::constants::{AES128_BLOCKSIZE, HEADER_MINSIZE, IFAC_MIN_SIZE, TOKEN_OVERHEAD};
713    let numerator = mtu.saturating_sub(IFAC_MIN_SIZE + HEADER_MINSIZE + TOKEN_OVERHEAD);
714    ((numerator / AES128_BLOCKSIZE) * AES128_BLOCKSIZE).saturating_sub(1)
715}
716
717/// Resolve the link MTU from the signalled value: absent or 0 means the
718/// default Reticulum MTU; anything else is clamped to
719/// `[LINK_MTU_MIN, LINK_MTU_MAX]`.
720fn effective_link_mtu(signalled_mtu: Option<u32>) -> u32 {
721    signalled_mtu
722        .filter(|mtu| *mtu != 0)
723        .map(|mtu| mtu.clamp(LINK_MTU_MIN, LINK_MTU_MAX))
724        .unwrap_or(MTU as u32)
725}
726use alloc::vec;
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731    use crate::constants::LINK_MDU;
732    use rns_crypto::FixedRng;
733
734    fn make_rng(seed: u8) -> FixedRng {
735        FixedRng::new(&[seed; 128])
736    }
737
738    fn active_link_pair() -> (LinkEngine, LinkEngine, Vec<u8>) {
739        let mut rng_id = make_rng(0x01);
740        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
741        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
742        let dest_hash = [0xDD; 16];
743
744        let mut rng_init = make_rng(0x10);
745        let (mut initiator, request_data) = LinkEngine::new_initiator(
746            &dest_hash,
747            1,
748            LinkMode::Aes256Cbc,
749            Some(500),
750            100.0,
751            &mut rng_init,
752        );
753        let mut hashable = vec![0x00, 0x00];
754        hashable.extend_from_slice(&dest_hash);
755        hashable.push(0x00);
756        hashable.extend_from_slice(&request_data);
757        initiator.set_link_id_from_hashable(&hashable, request_data.len());
758
759        let mut rng_resp = make_rng(0x20);
760        let (mut responder, lrproof_data) = LinkEngine::new_responder(
761            &dest_sig_prv,
762            &dest_sig_pub_bytes,
763            &request_data,
764            &hashable,
765            &dest_hash,
766            1,
767            100.5,
768            &mut rng_resp,
769        )
770        .unwrap();
771        let mut rng_lrrtt = make_rng(0x30);
772        let (lrrtt_encrypted, _) = initiator
773            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
774            .unwrap();
775        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
776
777        (initiator, responder, lrrtt_encrypted)
778    }
779
780    #[test]
781    fn test_compute_mdu_default() {
782        assert_eq!(compute_mdu(500), LINK_MDU);
783    }
784
785    #[test]
786    fn zero_mtu_signalling_falls_back_to_reticulum_mtu() {
787        let mut rng_id = make_rng(0x01);
788        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
789        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
790        let dest_hash = [0xDD; 16];
791
792        let mut rng_init = make_rng(0x10);
793        let (mut initiator, request_data) = LinkEngine::new_initiator(
794            &dest_hash,
795            1,
796            LinkMode::Aes256Cbc,
797            Some(0),
798            100.0,
799            &mut rng_init,
800        );
801        let mut hashable = vec![0x00, 0x00];
802        hashable.extend_from_slice(&dest_hash);
803        hashable.push(0x00);
804        hashable.extend_from_slice(&request_data);
805        initiator.set_link_id_from_hashable(&hashable, request_data.len());
806
807        let mut rng_resp = make_rng(0x20);
808        let (responder, lrproof_data) = LinkEngine::new_responder(
809            &dest_sig_prv,
810            &dest_sig_pub_bytes,
811            &request_data,
812            &hashable,
813            &dest_hash,
814            1,
815            100.5,
816            &mut rng_resp,
817        )
818        .unwrap();
819
820        let mut rng_lrrtt = make_rng(0x30);
821        initiator
822            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
823            .unwrap();
824
825        assert_eq!(initiator.mtu(), MTU as u32);
826        assert_eq!(responder.mtu(), MTU as u32);
827        assert_eq!(initiator.mdu(), LINK_MDU);
828        assert_eq!(responder.mdu(), LINK_MDU);
829    }
830
831    #[test]
832    fn test_full_handshake() {
833        // Setup: destination identity (for responder)
834        let mut rng_id = make_rng(0x01);
835        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
836        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
837
838        let dest_hash = [0xDD; 16];
839        let mode = LinkMode::Aes256Cbc;
840
841        // Step 1: Initiator creates link request
842        let mut rng_init = make_rng(0x10);
843        let (mut initiator, request_data) =
844            LinkEngine::new_initiator(&dest_hash, 1, mode, Some(500), 100.0, &mut rng_init);
845        assert_eq!(initiator.state(), LinkState::Pending);
846
847        // Simulate packet packing: build a fake hashable part
848        // In real usage, the caller packs a LINKREQUEST packet and calls set_link_id_from_hashable
849        let mut hashable = Vec::new();
850        hashable.push(0x00); // flags byte (lower nibble)
851        hashable.push(0x00); // hops
852        hashable.extend_from_slice(&dest_hash);
853        hashable.push(0x00); // context
854        hashable.extend_from_slice(&request_data);
855
856        initiator.set_link_id_from_hashable(&hashable, request_data.len());
857        assert_ne!(initiator.link_id(), &[0u8; 16]);
858
859        // Step 2: Responder receives link request
860        let mut rng_resp = make_rng(0x20);
861        let (mut responder, lrproof_data) = LinkEngine::new_responder(
862            &dest_sig_prv,
863            &dest_sig_pub_bytes,
864            &request_data,
865            &hashable,
866            &dest_hash,
867            1,
868            100.5,
869            &mut rng_resp,
870        )
871        .unwrap();
872        assert_eq!(responder.state(), LinkState::Handshake);
873        assert_eq!(responder.link_id(), initiator.link_id());
874
875        // Step 3: Initiator validates LRPROOF
876        let mut rng_lrrtt = make_rng(0x30);
877        assert!(initiator
878            .handle_lrproof_with_hops(
879                &[0xAA; 3],
880                &dest_sig_pub_bytes,
881                Some(8),
882                100.6,
883                &mut rng_lrrtt,
884            )
885            .is_err());
886        assert_eq!(initiator.expected_hops(), 1);
887        let mismatched_mode_proof = handshake::build_lrproof(
888            initiator.link_id(),
889            lrproof_data[64..96].try_into().unwrap(),
890            &dest_sig_pub_bytes,
891            &dest_sig_prv,
892            Some(500),
893            LinkMode::Aes128Cbc,
894        );
895        assert_eq!(
896            initiator
897                .handle_lrproof_with_hops(
898                    &mismatched_mode_proof,
899                    &dest_sig_pub_bytes,
900                    Some(9),
901                    100.65,
902                    &mut rng_lrrtt,
903                )
904                .unwrap_err(),
905            LinkError::UnsupportedMode,
906        );
907        assert_eq!(initiator.expected_hops(), 1);
908        let mut invalid_lrproof = lrproof_data.clone();
909        invalid_lrproof[0] ^= 0x01;
910        assert!(initiator
911            .handle_lrproof_with_hops(
912                &invalid_lrproof,
913                &dest_sig_pub_bytes,
914                Some(7),
915                100.7,
916                &mut rng_lrrtt,
917            )
918            .is_err());
919        assert_eq!(initiator.expected_hops(), 1);
920        let (lrrtt_encrypted, actions) = initiator
921            .handle_lrproof_with_hops(
922                &lrproof_data,
923                &dest_sig_pub_bytes,
924                Some(4),
925                100.8,
926                &mut rng_lrrtt,
927            )
928            .unwrap();
929        assert_eq!(initiator.state(), LinkState::Active);
930        assert!(initiator.rtt().is_some());
931        assert_eq!(initiator.rebalanced_at(), Some(100.8));
932        assert_eq!(actions.len(), 2); // StateChanged + LinkEstablished
933
934        // Step 4: Responder handles LRRTT
935        let actions = responder
936            .handle_lrrtt_with_hops(&lrrtt_encrypted, Some(4), 101.0)
937            .unwrap();
938        assert_eq!(responder.state(), LinkState::Active);
939        assert_eq!(initiator.expected_hops(), 4);
940        assert_eq!(responder.expected_hops(), 4);
941        assert_eq!(responder.rebalanced_at(), None);
942
943        // A later valid proof cannot rewrite the first authenticated
944        // rebalance, even if it reports another hop metric.
945        initiator.state = LinkState::Pending;
946        let mut repeated_rng = make_rng(0x31);
947        initiator
948            .handle_lrproof_with_hops(
949                &lrproof_data,
950                &dest_sig_pub_bytes,
951                Some(6),
952                101.1,
953                &mut repeated_rng,
954            )
955            .unwrap();
956        assert_eq!(initiator.expected_hops(), 4);
957        assert_eq!(initiator.rebalanced_at(), Some(100.8));
958
959        initiator.record_outbound_traffic(48);
960        initiator.record_inbound_traffic(32);
961        assert_eq!((initiator.tx_packets(), initiator.tx_bytes()), (1, 48));
962        assert_eq!((initiator.rx_packets(), initiator.rx_bytes()), (1, 32));
963        assert!(responder.rtt().is_some());
964        assert_eq!(actions.len(), 2);
965    }
966
967    #[test]
968    fn test_encrypt_decrypt_after_handshake() {
969        let mut rng_id = make_rng(0x01);
970        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
971        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
972        let dest_hash = [0xDD; 16];
973
974        let mut rng_init = make_rng(0x10);
975        let (mut initiator, request_data) = LinkEngine::new_initiator(
976            &dest_hash,
977            1,
978            LinkMode::Aes256Cbc,
979            Some(500),
980            100.0,
981            &mut rng_init,
982        );
983        let mut hashable = Vec::new();
984        hashable.push(0x00);
985        hashable.push(0x00);
986        hashable.extend_from_slice(&dest_hash);
987        hashable.push(0x00);
988        hashable.extend_from_slice(&request_data);
989        initiator.set_link_id_from_hashable(&hashable, request_data.len());
990
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
1004        let mut rng_lrrtt = make_rng(0x30);
1005        let (lrrtt_encrypted, _) = initiator
1006            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1007            .unwrap();
1008        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1009
1010        // Now both sides are ACTIVE — test encrypt/decrypt
1011        let mut rng_enc = make_rng(0x40);
1012        let plaintext = b"Hello over encrypted link!";
1013        let ciphertext = initiator.encrypt(plaintext, &mut rng_enc).unwrap();
1014        let decrypted = responder.decrypt(&ciphertext).unwrap();
1015        assert_eq!(decrypted, plaintext);
1016
1017        // And in reverse
1018        let mut rng_enc2 = make_rng(0x50);
1019        let ciphertext2 = responder.encrypt(b"Reply!", &mut rng_enc2).unwrap();
1020        let decrypted2 = initiator.decrypt(&ciphertext2).unwrap();
1021        assert_eq!(decrypted2, b"Reply!");
1022    }
1023
1024    #[test]
1025    fn test_tick_establishment_timeout() {
1026        let mut rng = make_rng(0x10);
1027        let dest_hash = [0xDD; 16];
1028        let (mut engine, _) =
1029            LinkEngine::new_initiator(&dest_hash, 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1030        // Timeout = 6.0 + 6.0 * 1 = 12.0s → expires at 112.0
1031
1032        // Before timeout — no state change
1033        let actions = engine.tick(110.0);
1034        assert!(actions.is_empty());
1035
1036        // After timeout
1037        let actions = engine.tick(113.0);
1038        assert_eq!(actions.len(), 1);
1039        assert_eq!(engine.state(), LinkState::Closed);
1040    }
1041
1042    #[test]
1043    fn test_tick_stale_and_close() {
1044        let mut rng_id = make_rng(0x01);
1045        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1046        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1047        let dest_hash = [0xDD; 16];
1048
1049        let mut rng_init = make_rng(0x10);
1050        let (mut initiator, request_data) = LinkEngine::new_initiator(
1051            &dest_hash,
1052            1,
1053            LinkMode::Aes256Cbc,
1054            Some(500),
1055            100.0,
1056            &mut rng_init,
1057        );
1058        let mut hashable = Vec::new();
1059        hashable.push(0x00);
1060        hashable.push(0x00);
1061        hashable.extend_from_slice(&dest_hash);
1062        hashable.push(0x00);
1063        hashable.extend_from_slice(&request_data);
1064        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1065
1066        let mut rng_resp = make_rng(0x20);
1067        let (_, lrproof_data) = LinkEngine::new_responder(
1068            &dest_sig_prv,
1069            &dest_sig_pub_bytes,
1070            &request_data,
1071            &hashable,
1072            &dest_hash,
1073            1,
1074            100.5,
1075            &mut rng_resp,
1076        )
1077        .unwrap();
1078
1079        let mut rng_lrrtt = make_rng(0x30);
1080        initiator
1081            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1082            .unwrap();
1083        assert_eq!(initiator.state(), LinkState::Active);
1084
1085        // Advance time past stale_time
1086        let stale_time = initiator.stale_time;
1087        let actions = initiator.tick(100.8 + stale_time + 1.0);
1088        assert_eq!(initiator.state(), LinkState::Stale);
1089        assert_eq!(actions.len(), 1);
1090
1091        // Next tick: STALE → CLOSED
1092        let actions = initiator.tick(100.8 + stale_time + 2.0);
1093        assert_eq!(initiator.state(), LinkState::Closed);
1094        assert_eq!(actions.len(), 1);
1095    }
1096
1097    #[test]
1098    fn test_needs_keepalive() {
1099        let mut rng_id = make_rng(0x01);
1100        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1101        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1102        let dest_hash = [0xDD; 16];
1103
1104        let mut rng_init = make_rng(0x10);
1105        let (mut initiator, request_data) = LinkEngine::new_initiator(
1106            &dest_hash,
1107            1,
1108            LinkMode::Aes256Cbc,
1109            Some(500),
1110            100.0,
1111            &mut rng_init,
1112        );
1113        let mut hashable = Vec::new();
1114        hashable.push(0x00);
1115        hashable.push(0x00);
1116        hashable.extend_from_slice(&dest_hash);
1117        hashable.push(0x00);
1118        hashable.extend_from_slice(&request_data);
1119        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1120
1121        let mut rng_resp = make_rng(0x20);
1122        let (_, lrproof_data) = LinkEngine::new_responder(
1123            &dest_sig_prv,
1124            &dest_sig_pub_bytes,
1125            &request_data,
1126            &hashable,
1127            &dest_hash,
1128            1,
1129            100.5,
1130            &mut rng_resp,
1131        )
1132        .unwrap();
1133
1134        let mut rng_lrrtt = make_rng(0x30);
1135        initiator
1136            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1137            .unwrap();
1138
1139        let ka = initiator.keepalive_interval();
1140        // Not yet
1141        assert!(!initiator.needs_keepalive(100.8 + ka - 1.0));
1142        // Past keepalive
1143        assert!(initiator.needs_keepalive(100.8 + ka + 1.0));
1144    }
1145
1146    #[test]
1147    fn initiator_probes_when_outbound_is_quiet_despite_recent_inbound() {
1148        let (mut initiator, _, _) = active_link_pair();
1149        let keepalive = initiator.keepalive_interval();
1150        let now = 1_000.0;
1151
1152        initiator.record_inbound(now - 0.1);
1153        initiator.record_outbound(now - keepalive - 0.1, false);
1154
1155        assert!(initiator.needs_keepalive(now));
1156    }
1157
1158    #[test]
1159    fn initiator_probes_when_inbound_is_quiet_despite_recent_outbound() {
1160        let (mut initiator, _, _) = active_link_pair();
1161        let keepalive = initiator.keepalive_interval();
1162        let now = 1_000.0;
1163
1164        initiator.record_inbound(now - keepalive - 0.1);
1165        initiator.record_outbound(now - 0.1, false);
1166
1167        assert!(initiator.needs_keepalive(now));
1168    }
1169
1170    #[test]
1171    fn initiator_does_not_probe_when_both_directions_are_recent() {
1172        let (mut initiator, _, _) = active_link_pair();
1173        let now = 1_000.0;
1174
1175        initiator.record_inbound(now - 0.1);
1176        initiator.record_outbound(now - 0.1, false);
1177
1178        assert!(!initiator.needs_keepalive(now));
1179    }
1180
1181    #[test]
1182    fn test_needs_keepalive_responder() {
1183        let mut rng_id = make_rng(0x01);
1184        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1185        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1186        let dest_hash = [0xDD; 16];
1187
1188        let mut rng_init = make_rng(0x10);
1189        let (mut initiator, request_data) = LinkEngine::new_initiator(
1190            &dest_hash,
1191            1,
1192            LinkMode::Aes256Cbc,
1193            Some(500),
1194            100.0,
1195            &mut rng_init,
1196        );
1197        let mut hashable = Vec::new();
1198        hashable.push(0x00);
1199        hashable.push(0x00);
1200        hashable.extend_from_slice(&dest_hash);
1201        hashable.push(0x00);
1202        hashable.extend_from_slice(&request_data);
1203        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1204
1205        let mut rng_resp = make_rng(0x20);
1206        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1207            &dest_sig_prv,
1208            &dest_sig_pub_bytes,
1209            &request_data,
1210            &hashable,
1211            &dest_hash,
1212            1,
1213            100.5,
1214            &mut rng_resp,
1215        )
1216        .unwrap();
1217
1218        let mut rng_lrrtt = make_rng(0x30);
1219        let (lrrtt_encrypted, _) = initiator
1220            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1221            .unwrap();
1222        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1223
1224        let ka = responder.keepalive_interval();
1225        // Only the initiator emits probes. The responder replies to probes.
1226        assert!(!responder.needs_keepalive(101.0 + ka - 1.0));
1227        assert!(!responder.needs_keepalive(101.0 + ka + 1.0));
1228    }
1229
1230    #[test]
1231    fn responder_replies_only_to_probe_and_only_when_outbound_is_quiet() {
1232        let (_, mut responder, _) = active_link_pair();
1233        let keepalive = responder.keepalive_interval();
1234        let now = 1_000.0;
1235
1236        responder.record_outbound(now - keepalive - 0.1, false);
1237        assert!(responder.should_reply_keepalive(&[0xff], now));
1238        assert!(!responder.should_reply_keepalive(&[0xfe], now));
1239        assert!(!responder.should_reply_keepalive(&[], now));
1240
1241        responder.record_outbound(now - 0.1, false);
1242        assert!(!responder.should_reply_keepalive(&[0xff], now));
1243    }
1244
1245    #[test]
1246    fn initiator_never_replies_to_keepalive_probe() {
1247        let (mut initiator, _, _) = active_link_pair();
1248        let keepalive = initiator.keepalive_interval();
1249        let now = 1_000.0;
1250        initiator.record_outbound(now - keepalive - 0.1, false);
1251
1252        assert!(!initiator.should_reply_keepalive(&[0xff], now));
1253    }
1254
1255    #[test]
1256    fn packet_delivery_proofs_are_bidirectional_and_peer_authenticated() {
1257        let (initiator, responder, _) = active_link_pair();
1258        let packet_hash = [0x42; 32];
1259
1260        let initiator_proof = initiator.sign_packet_hash(&packet_hash);
1261        assert!(responder.validate_packet_proof(&packet_hash, &initiator_proof));
1262        assert!(!initiator.validate_packet_proof(&packet_hash, &initiator_proof));
1263
1264        let responder_proof = responder.sign_packet_hash(&packet_hash);
1265        assert!(initiator.validate_packet_proof(&packet_hash, &responder_proof));
1266        assert!(!responder.validate_packet_proof(&packet_hash, &responder_proof));
1267        assert!(!initiator.validate_packet_proof(&[0x43; 32], &responder_proof));
1268    }
1269
1270    #[test]
1271    fn test_teardown() {
1272        let mut rng = make_rng(0x10);
1273        let (mut engine, _) =
1274            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1275        let actions = engine.teardown();
1276        assert_eq!(engine.state(), LinkState::Closed);
1277        assert_eq!(actions.len(), 1);
1278
1279        // Teardown again is no-op
1280        let actions = engine.teardown();
1281        assert!(actions.is_empty());
1282    }
1283
1284    #[test]
1285    fn test_handle_teardown() {
1286        let mut rng = make_rng(0x10);
1287        let (mut engine, _) =
1288            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
1289        let actions = engine.handle_teardown();
1290        assert_eq!(engine.state(), LinkState::Closed);
1291        assert_eq!(actions.len(), 1);
1292        match &actions[0] {
1293            LinkAction::StateChanged { reason, .. } => {
1294                assert_eq!(*reason, Some(TeardownReason::DestinationClosed));
1295            }
1296            _ => panic!("Expected StateChanged"),
1297        }
1298    }
1299
1300    #[test]
1301    fn test_identify_over_link() {
1302        let mut rng_id = make_rng(0x01);
1303        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1304        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1305        let dest_hash = [0xDD; 16];
1306
1307        let mut rng_init = make_rng(0x10);
1308        let (mut initiator, request_data) = LinkEngine::new_initiator(
1309            &dest_hash,
1310            1,
1311            LinkMode::Aes256Cbc,
1312            Some(500),
1313            100.0,
1314            &mut rng_init,
1315        );
1316        let mut hashable = Vec::new();
1317        hashable.push(0x00);
1318        hashable.push(0x00);
1319        hashable.extend_from_slice(&dest_hash);
1320        hashable.push(0x00);
1321        hashable.extend_from_slice(&request_data);
1322        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1323
1324        let mut rng_resp = make_rng(0x20);
1325        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1326            &dest_sig_prv,
1327            &dest_sig_pub_bytes,
1328            &request_data,
1329            &hashable,
1330            &dest_hash,
1331            1,
1332            100.5,
1333            &mut rng_resp,
1334        )
1335        .unwrap();
1336
1337        let mut rng_lrrtt = make_rng(0x30);
1338        let (lrrtt_encrypted, _) = initiator
1339            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1340            .unwrap();
1341        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1342
1343        // Create identity to identify with
1344        let mut rng_ident = make_rng(0x40);
1345        let my_identity = rns_crypto::identity::Identity::new(&mut rng_ident);
1346
1347        // Initiator identifies itself to responder
1348        let mut rng_enc = make_rng(0x50);
1349        let identify_encrypted = initiator
1350            .build_identify(&my_identity, &mut rng_enc)
1351            .unwrap();
1352
1353        let actions = responder.handle_identify(&identify_encrypted).unwrap();
1354        assert_eq!(actions.len(), 1);
1355        match &actions[0] {
1356            LinkAction::RemoteIdentified {
1357                identity_hash,
1358                public_key,
1359                ..
1360            } => {
1361                assert_eq!(identity_hash, my_identity.hash());
1362                assert_eq!(public_key, &my_identity.get_public_key().unwrap());
1363            }
1364            _ => panic!("Expected RemoteIdentified"),
1365        }
1366
1367        // Valid repeated identifies are authenticated, but identification is
1368        // one-shot and cannot replace the first identity or emit callbacks.
1369        let mut rng_repeat = make_rng(0x51);
1370        let repeated = initiator
1371            .build_identify(&my_identity, &mut rng_repeat)
1372            .unwrap();
1373        assert!(responder.handle_identify(&repeated).unwrap().is_empty());
1374
1375        let mut rng_other = make_rng(0x60);
1376        let other_identity = rns_crypto::identity::Identity::new(&mut rng_other);
1377        let mut rng_other_enc = make_rng(0x61);
1378        let other = initiator
1379            .build_identify(&other_identity, &mut rng_other_enc)
1380            .unwrap();
1381        assert!(responder.handle_identify(&other).unwrap().is_empty());
1382        assert_eq!(
1383            responder.remote_identity().map(|(hash, _)| hash),
1384            Some(my_identity.hash())
1385        );
1386
1387        // Malformed repeats are still rejected cryptographically.
1388        let mut rng_bad = make_rng(0x70);
1389        let malformed = initiator
1390            .encrypt(b"invalid identify", &mut rng_bad)
1391            .unwrap();
1392        assert!(responder.handle_identify(&malformed).is_err());
1393        assert_eq!(
1394            responder.remote_identity().map(|(hash, _)| hash),
1395            Some(my_identity.hash())
1396        );
1397    }
1398
1399    #[test]
1400    fn test_aes128_mode_handshake() {
1401        let mut rng_id = make_rng(0x01);
1402        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1403        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1404        let dest_hash = [0xDD; 16];
1405
1406        let mut rng_init = make_rng(0x10);
1407        let (mut initiator, request_data) = LinkEngine::new_initiator(
1408            &dest_hash,
1409            1,
1410            LinkMode::Aes128Cbc,
1411            Some(500),
1412            100.0,
1413            &mut rng_init,
1414        );
1415        let mut hashable = Vec::new();
1416        hashable.push(0x00);
1417        hashable.push(0x00);
1418        hashable.extend_from_slice(&dest_hash);
1419        hashable.push(0x00);
1420        hashable.extend_from_slice(&request_data);
1421        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1422
1423        let mut rng_resp = make_rng(0x20);
1424        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1425            &dest_sig_prv,
1426            &dest_sig_pub_bytes,
1427            &request_data,
1428            &hashable,
1429            &dest_hash,
1430            1,
1431            100.5,
1432            &mut rng_resp,
1433        )
1434        .unwrap();
1435
1436        let mut rng_lrrtt = make_rng(0x30);
1437        let (lrrtt_encrypted, _) = initiator
1438            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1439            .unwrap();
1440        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1441
1442        assert_eq!(initiator.state(), LinkState::Active);
1443        assert_eq!(responder.state(), LinkState::Active);
1444        assert_eq!(initiator.mode(), LinkMode::Aes128Cbc);
1445
1446        // Verify encrypt/decrypt works
1447        let mut rng_enc = make_rng(0x40);
1448        let ct = initiator.encrypt(b"AES128 test", &mut rng_enc).unwrap();
1449        let pt = responder.decrypt(&ct).unwrap();
1450        assert_eq!(pt, b"AES128 test");
1451    }
1452
1453    /// Run LINKREQUEST + LRPROOF with the given signalled MTU. Returns the
1454    /// active initiator and the responder still waiting for its LRRTT.
1455    fn handshake_pair_with_mtu(mtu: Option<u32>) -> (LinkEngine, LinkEngine) {
1456        let mut rng_id = make_rng(0x01);
1457        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
1458        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
1459        let dest_hash = [0xDD; 16];
1460
1461        let mut rng_init = make_rng(0x10);
1462        let (mut initiator, request_data) = LinkEngine::new_initiator(
1463            &dest_hash,
1464            1,
1465            LinkMode::Aes256Cbc,
1466            mtu,
1467            100.0,
1468            &mut rng_init,
1469        );
1470        let mut hashable = vec![0x00, 0x00];
1471        hashable.extend_from_slice(&dest_hash);
1472        hashable.push(0x00);
1473        hashable.extend_from_slice(&request_data);
1474        initiator.set_link_id_from_hashable(&hashable, request_data.len());
1475
1476        let mut rng_resp = make_rng(0x20);
1477        let (responder, lrproof_data) = LinkEngine::new_responder(
1478            &dest_sig_prv,
1479            &dest_sig_pub_bytes,
1480            &request_data,
1481            &hashable,
1482            &dest_hash,
1483            1,
1484            100.5,
1485            &mut rng_resp,
1486        )
1487        .unwrap();
1488
1489        let mut rng_lrrtt = make_rng(0x30);
1490        initiator
1491            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1492            .unwrap();
1493        (initiator, responder)
1494    }
1495
1496    fn raw_rtt_payload(rtt: f64) -> Vec<u8> {
1497        let mut data = vec![0xcb];
1498        data.extend_from_slice(&rtt.to_be_bytes());
1499        data
1500    }
1501
1502    #[test]
1503    fn compute_mdu_keeps_values_for_regular_mtus() {
1504        assert_eq!(compute_mdu(500), 431);
1505        assert_eq!(compute_mdu(508), 431);
1506        assert_eq!(compute_mdu(1064), 991);
1507        assert_eq!(compute_mdu(8192), 8111);
1508        assert_eq!(compute_mdu(262_144), 262_063);
1509        assert_eq!(compute_mdu(LINK_MTU_MAX as usize), 2_097_071);
1510    }
1511
1512    #[test]
1513    fn compute_mdu_saturates_for_small_mtus() {
1514        for mtu in [0usize, 1, 50, 67, 68, 83] {
1515            assert_eq!(compute_mdu(mtu), 0, "mtu {mtu}");
1516        }
1517        assert_eq!(compute_mdu(LINK_MTU_MIN as usize), 15);
1518    }
1519
1520    #[test]
1521    fn effective_link_mtu_is_clamped_and_mdu_monotone() {
1522        assert_eq!(effective_link_mtu(None), MTU as u32);
1523        assert_eq!(effective_link_mtu(Some(0)), MTU as u32);
1524        assert_eq!(effective_link_mtu(Some(1)), LINK_MTU_MIN);
1525        assert_eq!(effective_link_mtu(Some(50)), LINK_MTU_MIN);
1526        assert_eq!(effective_link_mtu(Some(83)), LINK_MTU_MIN);
1527        assert_eq!(effective_link_mtu(Some(84)), 84);
1528        assert_eq!(effective_link_mtu(Some(500)), 500);
1529        assert_eq!(effective_link_mtu(Some(LINK_MTU_MAX)), LINK_MTU_MAX);
1530        assert_eq!(effective_link_mtu(Some(u32::MAX)), LINK_MTU_MAX);
1531
1532        let mut prev = 0usize;
1533        for mtu in [1u32, 50, 83, 84, 500, LINK_MTU_MAX] {
1534            let mdu = compute_mdu(effective_link_mtu(Some(mtu)) as usize);
1535            assert!(mdu >= 15, "mtu {mtu} gave mdu {mdu}");
1536            assert!(mdu >= prev, "mdu not monotone at mtu {mtu}");
1537            prev = mdu;
1538        }
1539    }
1540
1541    #[test]
1542    fn small_signalled_mtu_is_raised_to_minimum_on_both_sides() {
1543        let (initiator, mut responder) = handshake_pair_with_mtu(Some(70));
1544        assert_eq!(responder.mtu(), LINK_MTU_MIN);
1545        assert_eq!(responder.mdu(), 15);
1546        assert_eq!(initiator.mtu(), LINK_MTU_MIN);
1547        assert_eq!(initiator.mdu(), 15);
1548        assert_eq!(initiator.state(), LinkState::Active);
1549
1550        let mut rng = make_rng(0x40);
1551        let lrrtt = initiator.encrypt(&pack_rtt(0.3), &mut rng).unwrap();
1552        responder.handle_lrrtt(&lrrtt, 101.0).unwrap();
1553        assert_eq!(responder.state(), LinkState::Active);
1554
1555        let ct = initiator.encrypt(b"hello", &mut rng).unwrap();
1556        assert_eq!(responder.decrypt(&ct).unwrap(), b"hello");
1557    }
1558
1559    #[test]
1560    fn signalled_mtu_edge_values_do_not_panic() {
1561        for mtu in [0u32, 1, 50, 83, 84, 500, LINK_MTU_MAX] {
1562            let (initiator, responder) = handshake_pair_with_mtu(Some(mtu));
1563            assert!(responder.mtu() >= LINK_MTU_MIN);
1564            assert!(responder.mdu() >= 15);
1565            assert_eq!(initiator.mtu(), responder.mtu());
1566            assert_eq!(initiator.mdu(), responder.mdu());
1567        }
1568    }
1569
1570    #[test]
1571    fn non_finite_or_negative_lrrtt_is_rejected() {
1572        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0] {
1573            let (initiator, mut responder) = handshake_pair_with_mtu(Some(500));
1574            let mut rng = make_rng(0x40);
1575            let lrrtt = initiator.encrypt(&raw_rtt_payload(bad), &mut rng).unwrap();
1576            assert!(
1577                matches!(
1578                    responder.handle_lrrtt(&lrrtt, 101.0),
1579                    Err(LinkError::InvalidData)
1580                ),
1581                "rtt {bad}"
1582            );
1583            assert_eq!(responder.state(), LinkState::Handshake);
1584            assert_eq!(responder.rtt(), None);
1585            assert!(responder.keepalive_interval.is_finite());
1586            assert!(responder.stale_time.is_finite());
1587
1588            // The peer goes silent: the pending link still times out.
1589            let deadline = responder.request_time + responder.establishment_timeout;
1590            let actions = responder.tick(deadline + 1.0);
1591            assert_eq!(responder.state(), LinkState::Closed);
1592            assert_eq!(actions.len(), 1);
1593        }
1594    }
1595
1596    #[test]
1597    fn valid_lrrtt_updates_rtt_and_timers() {
1598        let (initiator, mut responder) = handshake_pair_with_mtu(Some(500));
1599        let mut rng = make_rng(0x40);
1600        // Larger than the responder's own measurement (0.5s), so it is selected.
1601        let lrrtt = initiator.encrypt(&pack_rtt(0.9), &mut rng).unwrap();
1602        responder.handle_lrrtt(&lrrtt, 101.0).unwrap();
1603        assert_eq!(responder.state(), LinkState::Active);
1604        assert_eq!(responder.rtt(), Some(0.9));
1605        assert_eq!(responder.keepalive_interval, compute_keepalive(0.9));
1606        assert_eq!(
1607            responder.stale_time,
1608            compute_stale_time(compute_keepalive(0.9))
1609        );
1610    }
1611
1612    #[test]
1613    fn non_finite_rtt_keeps_timers_finite_and_link_goes_stale() {
1614        let (_, mut responder, _) = active_link_pair();
1615        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1616            responder.set_rtt(bad);
1617            assert!(responder.keepalive_interval.is_finite(), "rtt {bad}");
1618            assert!(responder.stale_time.is_finite(), "rtt {bad}");
1619            assert!(responder.stale_time <= LINK_KEEPALIVE_MAX * 2.0);
1620        }
1621
1622        let stale_time = responder.stale_time;
1623        responder.tick(101.0 + stale_time + 1.0);
1624        assert_eq!(responder.state(), LinkState::Stale);
1625    }
1626}