Skip to main content

rns_core/link/
mod.rs

1pub mod types;
2pub mod handshake;
3pub mod crypto;
4pub mod keepalive;
5pub mod identify;
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_KEEPALIVE_MAX, MTU,
16    LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
17};
18
19pub use types::{LinkAction, LinkError, LinkId, LinkMode, LinkState, TeardownReason};
20
21use handshake::{
22    build_linkrequest_data, compute_link_id, pack_rtt, parse_linkrequest_data,
23    perform_key_exchange, unpack_rtt, validate_lrproof,
24};
25use crypto::{create_session_token, link_decrypt, link_encrypt};
26use keepalive::{
27    compute_establishment_timeout, compute_keepalive, compute_stale_time,
28    is_establishment_timeout, 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
44    // Peer keys
45    peer_pub_bytes: Option<[u8; 32]>,
46    peer_sig_pub_bytes: Option<[u8; 32]>,
47
48    // Session crypto
49    derived_key: Option<Vec<u8>>,
50    token: Option<Token>,
51
52    // Timing
53    request_time: f64,
54    activated_at: Option<f64>,
55    last_inbound: f64,
56    last_outbound: f64,
57    last_keepalive: f64,
58    last_proof: f64,
59    rtt: Option<f64>,
60    keepalive_interval: f64,
61    stale_time: f64,
62    establishment_timeout: f64,
63
64    // Identity
65    remote_identity: Option<([u8; 16], [u8; 64])>,
66    destination_hash: [u8; 16],
67
68    // MDU
69    mtu: u32,
70    mdu: usize,
71}
72
73impl LinkEngine {
74    /// Create a new initiator-side link engine.
75    ///
76    /// Returns `(engine, linkrequest_data)` — the caller must pack linkrequest_data
77    /// into a LINKREQUEST packet and send it.
78    pub fn new_initiator(
79        dest_hash: &[u8; 16],
80        hops: u8,
81        mode: LinkMode,
82        mtu: Option<u32>,
83        now: f64,
84        rng: &mut dyn Rng,
85    ) -> (Self, Vec<u8>) {
86        let prv = X25519PrivateKey::generate(rng);
87        let pub_bytes = prv.public_key().public_bytes();
88        let sig_prv = Ed25519PrivateKey::generate(rng);
89        let sig_pub_bytes = sig_prv.public_key().public_bytes();
90
91        let request_data = build_linkrequest_data(&pub_bytes, &sig_pub_bytes, mtu, mode);
92
93        let link_mtu = mtu.unwrap_or(MTU as u32);
94
95        let engine = LinkEngine {
96            link_id: [0u8; 16], // will be set after packet is built
97            state: LinkState::Pending,
98            is_initiator: true,
99            mode,
100            prv,
101            peer_pub_bytes: None,
102            peer_sig_pub_bytes: None,
103            derived_key: None,
104            token: None,
105            request_time: now,
106            activated_at: None,
107            last_inbound: now,
108            last_outbound: now,
109            last_keepalive: now,
110            last_proof: 0.0,
111            rtt: None,
112            keepalive_interval: LINK_KEEPALIVE_MAX,
113            stale_time: LINK_KEEPALIVE_MAX * 2.0,
114            establishment_timeout: compute_establishment_timeout(
115                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
116                hops,
117            ),
118            remote_identity: None,
119            destination_hash: *dest_hash,
120            mtu: link_mtu,
121            mdu: compute_mdu(link_mtu as usize),
122        };
123
124        (engine, request_data)
125    }
126
127    /// Set link_id from the hashable part of the packed LINKREQUEST packet.
128    ///
129    /// Must be called after packing the LINKREQUEST packet (since link_id depends
130    /// on the packet's hashable part).
131    pub fn set_link_id_from_hashable(&mut self, hashable_part: &[u8], data_len: usize) {
132        let extra = if data_len > LINK_ECPUBSIZE {
133            data_len - LINK_ECPUBSIZE
134        } else {
135            0
136        };
137        self.link_id = compute_link_id(hashable_part, extra);
138    }
139
140    /// Create a new responder-side link engine from an incoming LINKREQUEST.
141    ///
142    /// `owner_sig_prv` / `owner_sig_pub` are the destination's signing keys.
143    /// Returns `(engine, actions)` where actions include the LRPROOF data to send.
144    pub fn new_responder(
145        owner_sig_prv: &Ed25519PrivateKey,
146        owner_sig_pub_bytes: &[u8; 32],
147        linkrequest_data: &[u8],
148        hashable_part: &[u8],
149        dest_hash: &[u8; 16],
150        hops: u8,
151        now: f64,
152        rng: &mut dyn Rng,
153    ) -> Result<(Self, Vec<u8>), LinkError> {
154        let (peer_pub, peer_sig_pub, peer_mtu, mode) = parse_linkrequest_data(linkrequest_data)?;
155
156        let extra = if linkrequest_data.len() > LINK_ECPUBSIZE {
157            linkrequest_data.len() - LINK_ECPUBSIZE
158        } else {
159            0
160        };
161        let link_id = compute_link_id(hashable_part, extra);
162
163        // Generate ephemeral keys for this end
164        let prv = X25519PrivateKey::generate(rng);
165        let pub_bytes = prv.public_key().public_bytes();
166        let sig_pub_bytes = *owner_sig_pub_bytes;
167
168        // Perform ECDH + HKDF
169        let derived_key = perform_key_exchange(&prv, &peer_pub, &link_id, mode)?;
170        let token = create_session_token(&derived_key)?;
171
172        let link_mtu = peer_mtu.unwrap_or(MTU as u32);
173
174        // Build LRPROOF
175        let lrproof_data = handshake::build_lrproof(
176            &link_id,
177            &pub_bytes,
178            &sig_pub_bytes,
179            owner_sig_prv,
180            peer_mtu,
181            mode,
182        );
183
184        let engine = LinkEngine {
185            link_id,
186            state: LinkState::Handshake,
187            is_initiator: false,
188            mode,
189            prv,
190            peer_pub_bytes: Some(peer_pub),
191            peer_sig_pub_bytes: Some(peer_sig_pub),
192            derived_key: Some(derived_key),
193            token: Some(token),
194            request_time: now,
195            activated_at: None,
196            last_inbound: now,
197            last_outbound: now,
198            last_keepalive: now,
199            last_proof: 0.0,
200            rtt: None,
201            keepalive_interval: LINK_KEEPALIVE_MAX,
202            stale_time: LINK_KEEPALIVE_MAX * 2.0,
203            establishment_timeout: compute_establishment_timeout(
204                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
205                hops,
206            ),
207            remote_identity: None,
208            destination_hash: *dest_hash,
209            mtu: link_mtu,
210            mdu: compute_mdu(link_mtu as usize),
211        };
212
213        Ok((engine, lrproof_data))
214    }
215
216    /// Handle an incoming LRPROOF (initiator side).
217    ///
218    /// Validates the proof, performs ECDH, derives session key, returns LRRTT data
219    /// to be encrypted and sent.
220    pub fn handle_lrproof(
221        &mut self,
222        proof_data: &[u8],
223        peer_sig_pub_bytes: &[u8; 32],
224        now: f64,
225        rng: &mut dyn Rng,
226    ) -> Result<(Vec<u8>, Vec<LinkAction>), LinkError> {
227        if self.state != LinkState::Pending || !self.is_initiator {
228            return Err(LinkError::InvalidState);
229        }
230
231        let peer_sig_pub = Ed25519PublicKey::from_bytes(peer_sig_pub_bytes);
232
233        let (peer_pub, confirmed_mtu, confirmed_mode) =
234            validate_lrproof(proof_data, &self.link_id, &peer_sig_pub, peer_sig_pub_bytes)?;
235
236        if confirmed_mode != self.mode {
237            return Err(LinkError::UnsupportedMode);
238        }
239
240        self.peer_pub_bytes = Some(peer_pub);
241        self.peer_sig_pub_bytes = Some(*peer_sig_pub_bytes);
242
243        // ECDH + HKDF
244        let derived_key = perform_key_exchange(&self.prv, &peer_pub, &self.link_id, self.mode)?;
245        let token = create_session_token(&derived_key)?;
246
247        self.derived_key = Some(derived_key);
248        self.token = Some(token);
249
250        // Update MTU if confirmed
251        if let Some(mtu) = confirmed_mtu {
252            self.mtu = mtu;
253            self.mdu = compute_mdu(mtu as usize);
254        }
255
256        // Compute RTT and activate
257        let rtt = now - self.request_time;
258        self.rtt = Some(rtt);
259        self.state = LinkState::Active;
260        self.activated_at = Some(now);
261        self.last_inbound = now;
262        self.update_keepalive();
263
264        // Build encrypted LRRTT packet data
265        let rtt_packed = pack_rtt(rtt);
266        let rtt_encrypted = self.encrypt(&rtt_packed, rng)?;
267
268        let mut actions = Vec::new();
269        actions.push(LinkAction::StateChanged {
270            link_id: self.link_id,
271            new_state: LinkState::Active,
272            reason: None,
273        });
274        actions.push(LinkAction::LinkEstablished {
275            link_id: self.link_id,
276            rtt,
277            is_initiator: true,
278        });
279
280        Ok((rtt_encrypted, actions))
281    }
282
283    /// Handle an incoming LRRTT (responder side).
284    ///
285    /// Decrypts the RTT packet, activates the link.
286    pub fn handle_lrrtt(
287        &mut self,
288        encrypted_data: &[u8],
289        now: f64,
290    ) -> Result<Vec<LinkAction>, LinkError> {
291        if self.state != LinkState::Handshake || self.is_initiator {
292            return Err(LinkError::InvalidState);
293        }
294
295        let plaintext = self.decrypt(encrypted_data)?;
296        let initiator_rtt = unpack_rtt(&plaintext).ok_or(LinkError::InvalidData)?;
297
298        let measured_rtt = now - self.request_time;
299        let rtt = if measured_rtt > initiator_rtt { measured_rtt } else { initiator_rtt };
300
301        self.rtt = Some(rtt);
302        self.state = LinkState::Active;
303        self.activated_at = Some(now);
304        self.last_inbound = now;
305        self.update_keepalive();
306
307        let mut actions = Vec::new();
308        actions.push(LinkAction::StateChanged {
309            link_id: self.link_id,
310            new_state: LinkState::Active,
311            reason: None,
312        });
313        actions.push(LinkAction::LinkEstablished {
314            link_id: self.link_id,
315            rtt,
316            is_initiator: false,
317        });
318
319        Ok(actions)
320    }
321
322    /// Encrypt plaintext for transmission over this link.
323    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Result<Vec<u8>, LinkError> {
324        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
325        Ok(link_encrypt(token, plaintext, rng))
326    }
327
328    /// Decrypt ciphertext received on this link.
329    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, LinkError> {
330        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
331        link_decrypt(token, ciphertext)
332    }
333
334    /// Build LINKIDENTIFY data (encrypted).
335    pub fn build_identify(
336        &self,
337        identity: &rns_crypto::identity::Identity,
338        rng: &mut dyn Rng,
339    ) -> Result<Vec<u8>, LinkError> {
340        if self.state != LinkState::Active {
341            return Err(LinkError::InvalidState);
342        }
343        let plaintext = identify::build_identify_data(identity, &self.link_id)?;
344        self.encrypt(&plaintext, rng)
345    }
346
347    /// Handle incoming LINKIDENTIFY (encrypted data).
348    ///
349    /// Only responders (non-initiators) can receive LINKIDENTIFY (Python: Link.py:1017).
350    pub fn handle_identify(&mut self, encrypted_data: &[u8]) -> Result<Vec<LinkAction>, LinkError> {
351        if self.state != LinkState::Active || self.is_initiator {
352            return Err(LinkError::InvalidState);
353        }
354
355        let plaintext = self.decrypt(encrypted_data)?;
356        let (identity_hash, public_key) = identify::validate_identify_data(&plaintext, &self.link_id)?;
357        self.remote_identity = Some((identity_hash, public_key));
358
359        Ok(alloc::vec![LinkAction::RemoteIdentified {
360            link_id: self.link_id,
361            identity_hash,
362            public_key,
363        }])
364    }
365
366    /// Record that an inbound packet was received (updates timing).
367    ///
368    /// If the link is STALE, recovers to ACTIVE (Python: Link.py:987-988).
369    pub fn record_inbound(&mut self, now: f64) -> Vec<LinkAction> {
370        self.last_inbound = now;
371        if self.state == LinkState::Stale {
372            self.state = LinkState::Active;
373            return alloc::vec![LinkAction::StateChanged {
374                link_id: self.link_id,
375                new_state: LinkState::Active,
376                reason: None,
377            }];
378        }
379        Vec::new()
380    }
381
382    /// Record that a proof was received (updates timing for stale detection).
383    pub fn record_proof(&mut self, now: f64) {
384        self.last_proof = now;
385    }
386
387    /// Record that an outbound packet was sent (updates timing).
388    pub fn record_outbound(&mut self, now: f64, is_keepalive: bool) {
389        self.last_outbound = now;
390        if is_keepalive {
391            self.last_keepalive = now;
392        }
393    }
394
395    /// Periodic tick: check keepalive, stale, timeouts.
396    pub fn tick(&mut self, now: f64) -> Vec<LinkAction> {
397        let mut actions = Vec::new();
398
399        match self.state {
400            LinkState::Pending | LinkState::Handshake => {
401                if is_establishment_timeout(self.request_time, self.establishment_timeout, now) {
402                    self.state = LinkState::Closed;
403                    actions.push(LinkAction::StateChanged {
404                        link_id: self.link_id,
405                        new_state: LinkState::Closed,
406                        reason: Some(TeardownReason::Timeout),
407                    });
408                }
409            }
410            LinkState::Active => {
411                let activated = self.activated_at.unwrap_or(0.0);
412                // Python: max(max(self.last_inbound, self.last_proof), activated_at)
413                let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
414
415                if should_go_stale(last_inbound, self.stale_time, now) {
416                    self.state = LinkState::Stale;
417                    actions.push(LinkAction::StateChanged {
418                        link_id: self.link_id,
419                        new_state: LinkState::Stale,
420                        reason: None,
421                    });
422                }
423            }
424            LinkState::Stale => {
425                // In Python, STALE immediately sends teardown and closes
426                self.state = LinkState::Closed;
427                actions.push(LinkAction::StateChanged {
428                    link_id: self.link_id,
429                    new_state: LinkState::Closed,
430                    reason: Some(TeardownReason::Timeout),
431                });
432            }
433            LinkState::Closed => {}
434        }
435
436        actions
437    }
438
439    /// Check if a keepalive should be sent. Returns true if conditions are met.
440    pub fn needs_keepalive(&self, now: f64) -> bool {
441        if self.state != LinkState::Active {
442            return false;
443        }
444        let activated = self.activated_at.unwrap_or(0.0);
445        let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
446
447        // Only send keepalive when past keepalive interval from last inbound
448        if now < last_inbound + self.keepalive_interval {
449            return false;
450        }
451
452        should_send_keepalive(self.last_keepalive, self.keepalive_interval, now)
453    }
454
455    /// Tear down the link (initiator-initiated close).
456    pub fn teardown(&mut self) -> Vec<LinkAction> {
457        if self.state == LinkState::Closed {
458            return Vec::new();
459        }
460        self.state = LinkState::Closed;
461        let reason = if self.is_initiator {
462            TeardownReason::InitiatorClosed
463        } else {
464            TeardownReason::DestinationClosed
465        };
466        alloc::vec![LinkAction::StateChanged {
467            link_id: self.link_id,
468            new_state: LinkState::Closed,
469            reason: Some(reason),
470        }]
471    }
472
473    /// Handle incoming teardown (remote close).
474    pub fn handle_teardown(&mut self) -> Vec<LinkAction> {
475        if self.state == LinkState::Closed {
476            return Vec::new();
477        }
478        self.state = LinkState::Closed;
479        let reason = if self.is_initiator {
480            TeardownReason::DestinationClosed
481        } else {
482            TeardownReason::InitiatorClosed
483        };
484        alloc::vec![LinkAction::StateChanged {
485            link_id: self.link_id,
486            new_state: LinkState::Closed,
487            reason: Some(reason),
488        }]
489    }
490
491    // --- Queries ---
492
493    pub fn link_id(&self) -> &LinkId {
494        &self.link_id
495    }
496
497    pub fn state(&self) -> LinkState {
498        self.state
499    }
500
501    pub fn rtt(&self) -> Option<f64> {
502        self.rtt
503    }
504
505    pub fn mdu(&self) -> usize {
506        self.mdu
507    }
508
509    pub fn is_initiator(&self) -> bool {
510        self.is_initiator
511    }
512
513    pub fn mode(&self) -> LinkMode {
514        self.mode
515    }
516
517    pub fn remote_identity(&self) -> Option<&([u8; 16], [u8; 64])> {
518        self.remote_identity.as_ref()
519    }
520
521    pub fn destination_hash(&self) -> &[u8; 16] {
522        &self.destination_hash
523    }
524
525    /// Get the derived session key (needed for hole-punch token derivation).
526    pub fn derived_key(&self) -> Option<&[u8]> {
527        self.derived_key.as_deref()
528    }
529
530    pub fn keepalive_interval(&self) -> f64 {
531        self.keepalive_interval
532    }
533
534    /// Update the measured RTT (e.g., after path redirect to a direct link).
535    /// Also recalculates keepalive and stale timers.
536    pub fn set_rtt(&mut self, rtt: f64) {
537        self.rtt = Some(rtt);
538        self.update_keepalive();
539    }
540
541    /// Update the link MTU (e.g., after path redirect to a different interface).
542    pub fn set_mtu(&mut self, mtu: u32) {
543        self.mtu = mtu;
544        self.mdu = compute_mdu(mtu as usize);
545    }
546
547    // --- Internal ---
548
549    fn update_keepalive(&mut self) {
550        if let Some(rtt) = self.rtt {
551            self.keepalive_interval = compute_keepalive(rtt);
552            self.stale_time = compute_stale_time(self.keepalive_interval);
553        }
554    }
555}
556
557/// Compute link MDU from MTU.
558///
559/// MDU = floor((mtu - IFAC_MIN_SIZE - HEADER_MINSIZE - TOKEN_OVERHEAD) / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
560fn compute_mdu(mtu: usize) -> usize {
561    use crate::constants::{AES128_BLOCKSIZE, HEADER_MINSIZE, IFAC_MIN_SIZE, TOKEN_OVERHEAD};
562    let numerator = mtu.saturating_sub(IFAC_MIN_SIZE + HEADER_MINSIZE + TOKEN_OVERHEAD);
563    (numerator / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use crate::constants::LINK_MDU;
570    use rns_crypto::FixedRng;
571
572    fn make_rng(seed: u8) -> FixedRng {
573        FixedRng::new(&[seed; 128])
574    }
575
576    #[test]
577    fn test_compute_mdu_default() {
578        assert_eq!(compute_mdu(500), LINK_MDU);
579    }
580
581    #[test]
582    fn test_full_handshake() {
583        // Setup: destination identity (for responder)
584        let mut rng_id = make_rng(0x01);
585        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
586        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
587
588        let dest_hash = [0xDD; 16];
589        let mode = LinkMode::Aes256Cbc;
590
591        // Step 1: Initiator creates link request
592        let mut rng_init = make_rng(0x10);
593        let (mut initiator, request_data) = LinkEngine::new_initiator(
594            &dest_hash, 1, mode, Some(500), 100.0, &mut rng_init,
595        );
596        assert_eq!(initiator.state(), LinkState::Pending);
597
598        // Simulate packet packing: build a fake hashable part
599        // In real usage, the caller packs a LINKREQUEST packet and calls set_link_id_from_hashable
600        let mut hashable = Vec::new();
601        hashable.push(0x00); // flags byte (lower nibble)
602        hashable.push(0x00); // hops
603        hashable.extend_from_slice(&dest_hash);
604        hashable.push(0x00); // context
605        hashable.extend_from_slice(&request_data);
606
607        initiator.set_link_id_from_hashable(&hashable, request_data.len());
608        assert_ne!(initiator.link_id(), &[0u8; 16]);
609
610        // Step 2: Responder receives link request
611        let mut rng_resp = make_rng(0x20);
612        let (mut responder, lrproof_data) = LinkEngine::new_responder(
613            &dest_sig_prv,
614            &dest_sig_pub_bytes,
615            &request_data,
616            &hashable,
617            &dest_hash,
618            1,
619            100.5,
620            &mut rng_resp,
621        ).unwrap();
622        assert_eq!(responder.state(), LinkState::Handshake);
623        assert_eq!(responder.link_id(), initiator.link_id());
624
625        // Step 3: Initiator validates LRPROOF
626        let mut rng_lrrtt = make_rng(0x30);
627        let (lrrtt_encrypted, actions) = initiator.handle_lrproof(
628            &lrproof_data,
629            &dest_sig_pub_bytes,
630            100.8,
631            &mut rng_lrrtt,
632        ).unwrap();
633        assert_eq!(initiator.state(), LinkState::Active);
634        assert!(initiator.rtt().is_some());
635        assert_eq!(actions.len(), 2); // StateChanged + LinkEstablished
636
637        // Step 4: Responder handles LRRTT
638        let actions = responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
639        assert_eq!(responder.state(), LinkState::Active);
640        assert!(responder.rtt().is_some());
641        assert_eq!(actions.len(), 2);
642    }
643
644    #[test]
645    fn test_encrypt_decrypt_after_handshake() {
646        let mut rng_id = make_rng(0x01);
647        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
648        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
649        let dest_hash = [0xDD; 16];
650
651        let mut rng_init = make_rng(0x10);
652        let (mut initiator, request_data) = LinkEngine::new_initiator(
653            &dest_hash, 1, LinkMode::Aes256Cbc, Some(500), 100.0, &mut rng_init,
654        );
655        let mut hashable = Vec::new();
656        hashable.push(0x00);
657        hashable.push(0x00);
658        hashable.extend_from_slice(&dest_hash);
659        hashable.push(0x00);
660        hashable.extend_from_slice(&request_data);
661        initiator.set_link_id_from_hashable(&hashable, request_data.len());
662
663        let mut rng_resp = make_rng(0x20);
664        let (mut responder, lrproof_data) = LinkEngine::new_responder(
665            &dest_sig_prv, &dest_sig_pub_bytes, &request_data, &hashable,
666            &dest_hash, 1, 100.5, &mut rng_resp,
667        ).unwrap();
668
669        let mut rng_lrrtt = make_rng(0x30);
670        let (lrrtt_encrypted, _) = initiator.handle_lrproof(
671            &lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt,
672        ).unwrap();
673        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
674
675        // Now both sides are ACTIVE — test encrypt/decrypt
676        let mut rng_enc = make_rng(0x40);
677        let plaintext = b"Hello over encrypted link!";
678        let ciphertext = initiator.encrypt(plaintext, &mut rng_enc).unwrap();
679        let decrypted = responder.decrypt(&ciphertext).unwrap();
680        assert_eq!(decrypted, plaintext);
681
682        // And in reverse
683        let mut rng_enc2 = make_rng(0x50);
684        let ciphertext2 = responder.encrypt(b"Reply!", &mut rng_enc2).unwrap();
685        let decrypted2 = initiator.decrypt(&ciphertext2).unwrap();
686        assert_eq!(decrypted2, b"Reply!");
687    }
688
689    #[test]
690    fn test_tick_establishment_timeout() {
691        let mut rng = make_rng(0x10);
692        let dest_hash = [0xDD; 16];
693        let (mut engine, _) = LinkEngine::new_initiator(
694            &dest_hash, 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng,
695        );
696        // Timeout = 6.0 + 6.0 * 1 = 12.0s → expires at 112.0
697
698        // Before timeout — no state change
699        let actions = engine.tick(110.0);
700        assert!(actions.is_empty());
701
702        // After timeout
703        let actions = engine.tick(113.0);
704        assert_eq!(actions.len(), 1);
705        assert_eq!(engine.state(), LinkState::Closed);
706    }
707
708    #[test]
709    fn test_tick_stale_and_close() {
710        let mut rng_id = make_rng(0x01);
711        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
712        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
713        let dest_hash = [0xDD; 16];
714
715        let mut rng_init = make_rng(0x10);
716        let (mut initiator, request_data) = LinkEngine::new_initiator(
717            &dest_hash, 1, LinkMode::Aes256Cbc, Some(500), 100.0, &mut rng_init,
718        );
719        let mut hashable = Vec::new();
720        hashable.push(0x00);
721        hashable.push(0x00);
722        hashable.extend_from_slice(&dest_hash);
723        hashable.push(0x00);
724        hashable.extend_from_slice(&request_data);
725        initiator.set_link_id_from_hashable(&hashable, request_data.len());
726
727        let mut rng_resp = make_rng(0x20);
728        let (_, lrproof_data) = LinkEngine::new_responder(
729            &dest_sig_prv, &dest_sig_pub_bytes, &request_data, &hashable,
730            &dest_hash, 1, 100.5, &mut rng_resp,
731        ).unwrap();
732
733        let mut rng_lrrtt = make_rng(0x30);
734        initiator.handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt).unwrap();
735        assert_eq!(initiator.state(), LinkState::Active);
736
737        // Advance time past stale_time
738        let stale_time = initiator.stale_time;
739        let actions = initiator.tick(100.8 + stale_time + 1.0);
740        assert_eq!(initiator.state(), LinkState::Stale);
741        assert_eq!(actions.len(), 1);
742
743        // Next tick: STALE → CLOSED
744        let actions = initiator.tick(100.8 + stale_time + 2.0);
745        assert_eq!(initiator.state(), LinkState::Closed);
746        assert_eq!(actions.len(), 1);
747    }
748
749    #[test]
750    fn test_needs_keepalive() {
751        let mut rng_id = make_rng(0x01);
752        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
753        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
754        let dest_hash = [0xDD; 16];
755
756        let mut rng_init = make_rng(0x10);
757        let (mut initiator, request_data) = LinkEngine::new_initiator(
758            &dest_hash, 1, LinkMode::Aes256Cbc, Some(500), 100.0, &mut rng_init,
759        );
760        let mut hashable = Vec::new();
761        hashable.push(0x00);
762        hashable.push(0x00);
763        hashable.extend_from_slice(&dest_hash);
764        hashable.push(0x00);
765        hashable.extend_from_slice(&request_data);
766        initiator.set_link_id_from_hashable(&hashable, request_data.len());
767
768        let mut rng_resp = make_rng(0x20);
769        let (_, lrproof_data) = LinkEngine::new_responder(
770            &dest_sig_prv, &dest_sig_pub_bytes, &request_data, &hashable,
771            &dest_hash, 1, 100.5, &mut rng_resp,
772        ).unwrap();
773
774        let mut rng_lrrtt = make_rng(0x30);
775        initiator.handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt).unwrap();
776
777        let ka = initiator.keepalive_interval();
778        // Not yet
779        assert!(!initiator.needs_keepalive(100.8 + ka - 1.0));
780        // Past keepalive
781        assert!(initiator.needs_keepalive(100.8 + ka + 1.0));
782    }
783
784    #[test]
785    fn test_needs_keepalive_responder() {
786        let mut rng_id = make_rng(0x01);
787        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
788        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
789        let dest_hash = [0xDD; 16];
790
791        let mut rng_init = make_rng(0x10);
792        let (mut initiator, request_data) = LinkEngine::new_initiator(
793            &dest_hash, 1, LinkMode::Aes256Cbc, Some(500), 100.0, &mut rng_init,
794        );
795        let mut hashable = Vec::new();
796        hashable.push(0x00);
797        hashable.push(0x00);
798        hashable.extend_from_slice(&dest_hash);
799        hashable.push(0x00);
800        hashable.extend_from_slice(&request_data);
801        initiator.set_link_id_from_hashable(&hashable, request_data.len());
802
803        let mut rng_resp = make_rng(0x20);
804        let (mut responder, lrproof_data) = LinkEngine::new_responder(
805            &dest_sig_prv, &dest_sig_pub_bytes, &request_data, &hashable,
806            &dest_hash, 1, 100.5, &mut rng_resp,
807        ).unwrap();
808
809        let mut rng_lrrtt = make_rng(0x30);
810        let (lrrtt_encrypted, _) = initiator.handle_lrproof(
811            &lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt,
812        ).unwrap();
813        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
814
815        let ka = responder.keepalive_interval();
816        // Responder should also send keepalives
817        assert!(!responder.needs_keepalive(101.0 + ka - 1.0));
818        assert!(responder.needs_keepalive(101.0 + ka + 1.0));
819    }
820
821    #[test]
822    fn test_teardown() {
823        let mut rng = make_rng(0x10);
824        let (mut engine, _) = LinkEngine::new_initiator(
825            &[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng,
826        );
827        let actions = engine.teardown();
828        assert_eq!(engine.state(), LinkState::Closed);
829        assert_eq!(actions.len(), 1);
830
831        // Teardown again is no-op
832        let actions = engine.teardown();
833        assert!(actions.is_empty());
834    }
835
836    #[test]
837    fn test_handle_teardown() {
838        let mut rng = make_rng(0x10);
839        let (mut engine, _) = LinkEngine::new_initiator(
840            &[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng,
841        );
842        let actions = engine.handle_teardown();
843        assert_eq!(engine.state(), LinkState::Closed);
844        assert_eq!(actions.len(), 1);
845        match &actions[0] {
846            LinkAction::StateChanged { reason, .. } => {
847                assert_eq!(*reason, Some(TeardownReason::DestinationClosed));
848            }
849            _ => panic!("Expected StateChanged"),
850        }
851    }
852
853    #[test]
854    fn test_identify_over_link() {
855        let mut rng_id = make_rng(0x01);
856        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
857        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
858        let dest_hash = [0xDD; 16];
859
860        let mut rng_init = make_rng(0x10);
861        let (mut initiator, request_data) = LinkEngine::new_initiator(
862            &dest_hash, 1, LinkMode::Aes256Cbc, Some(500), 100.0, &mut rng_init,
863        );
864        let mut hashable = Vec::new();
865        hashable.push(0x00);
866        hashable.push(0x00);
867        hashable.extend_from_slice(&dest_hash);
868        hashable.push(0x00);
869        hashable.extend_from_slice(&request_data);
870        initiator.set_link_id_from_hashable(&hashable, request_data.len());
871
872        let mut rng_resp = make_rng(0x20);
873        let (mut responder, lrproof_data) = LinkEngine::new_responder(
874            &dest_sig_prv, &dest_sig_pub_bytes, &request_data, &hashable,
875            &dest_hash, 1, 100.5, &mut rng_resp,
876        ).unwrap();
877
878        let mut rng_lrrtt = make_rng(0x30);
879        let (lrrtt_encrypted, _) = initiator.handle_lrproof(
880            &lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt,
881        ).unwrap();
882        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
883
884        // Create identity to identify with
885        let mut rng_ident = make_rng(0x40);
886        let my_identity = rns_crypto::identity::Identity::new(&mut rng_ident);
887
888        // Initiator identifies itself to responder
889        let mut rng_enc = make_rng(0x50);
890        let identify_encrypted = initiator.build_identify(&my_identity, &mut rng_enc).unwrap();
891
892        let actions = responder.handle_identify(&identify_encrypted).unwrap();
893        assert_eq!(actions.len(), 1);
894        match &actions[0] {
895            LinkAction::RemoteIdentified { identity_hash, public_key, .. } => {
896                assert_eq!(identity_hash, my_identity.hash());
897                assert_eq!(public_key, &my_identity.get_public_key().unwrap());
898            }
899            _ => panic!("Expected RemoteIdentified"),
900        }
901    }
902
903    #[test]
904    fn test_aes128_mode_handshake() {
905        let mut rng_id = make_rng(0x01);
906        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
907        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
908        let dest_hash = [0xDD; 16];
909
910        let mut rng_init = make_rng(0x10);
911        let (mut initiator, request_data) = LinkEngine::new_initiator(
912            &dest_hash, 1, LinkMode::Aes128Cbc, Some(500), 100.0, &mut rng_init,
913        );
914        let mut hashable = Vec::new();
915        hashable.push(0x00);
916        hashable.push(0x00);
917        hashable.extend_from_slice(&dest_hash);
918        hashable.push(0x00);
919        hashable.extend_from_slice(&request_data);
920        initiator.set_link_id_from_hashable(&hashable, request_data.len());
921
922        let mut rng_resp = make_rng(0x20);
923        let (mut responder, lrproof_data) = LinkEngine::new_responder(
924            &dest_sig_prv, &dest_sig_pub_bytes, &request_data, &hashable,
925            &dest_hash, 1, 100.5, &mut rng_resp,
926        ).unwrap();
927
928        let mut rng_lrrtt = make_rng(0x30);
929        let (lrrtt_encrypted, _) = initiator.handle_lrproof(
930            &lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt,
931        ).unwrap();
932        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
933
934        assert_eq!(initiator.state(), LinkState::Active);
935        assert_eq!(responder.state(), LinkState::Active);
936        assert_eq!(initiator.mode(), LinkMode::Aes128Cbc);
937
938        // Verify encrypt/decrypt works
939        let mut rng_enc = make_rng(0x40);
940        let ct = initiator.encrypt(b"AES128 test", &mut rng_enc).unwrap();
941        let pt = responder.decrypt(&ct).unwrap();
942        assert_eq!(pt, b"AES128 test");
943    }
944}