1#![warn(missing_docs)]
2use std::collections::HashMap;
35use std::collections::HashSet;
36use std::sync::Arc;
37
38use bytes::Bytes;
39use rings_core::dht::Did;
40use serde::de::DeserializeOwned;
41use serde::Deserialize;
42use serde::Serialize;
43
44use crate::extension::ext::Ctx;
45use crate::extension::ext::Interpret;
46use crate::extension::ext::MaybeSend;
47use crate::extension::ext::Protocol;
48use crate::extension::ext::Reject;
49use crate::extension::ext::Scope;
50use crate::extension::ext::Transition;
51use crate::extension::ext::Wire;
52use crate::extension::transport::Frame;
53use crate::extension::transport::Initiator;
54use crate::extension::transport::SessionId;
55use crate::extension::transport::SessionKey;
56use crate::extension::transport::TransportKind;
57
58pub const TCP: &str = "tcp";
60pub const UDP: &str = "udp";
62
63#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
66pub enum RelayCommand<T> {
67 RegisterService {
69 name: String,
71 target: T,
73 },
74 UnregisterService {
76 name: String,
78 },
79 Accepted {
85 token: u64,
87 peer: Did,
89 service: String,
91 },
92 Untrack {
95 peer: Did,
97 session: SessionId,
99 initiator: Initiator,
101 },
102}
103
104pub enum RelayEvent<T> {
107 Command(RelayCommand<T>),
109 Frame {
111 from: Did,
113 frame: Frame,
115 },
116}
117
118#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum RelayEffect<T> {
121 Connect {
123 key: SessionKey,
125 target: T,
127 kind: TransportKind,
129 },
130 Write {
132 key: SessionKey,
134 bytes: Bytes,
136 },
137 Shutdown {
139 key: SessionKey,
141 },
142 Close {
144 key: SessionKey,
146 },
147 SendClose {
151 to: Did,
153 session: SessionId,
155 from_opener: bool,
157 },
158 OpenAccepted {
162 token: u64,
164 key: SessionKey,
166 service: String,
168 },
169}
170
171#[derive(Clone)]
175pub struct RelayState<T> {
176 services: Arc<HashMap<String, T>>,
177 sessions: HashSet<SessionKey>,
178 next_session: u64,
182}
183
184impl<T> Default for RelayState<T> {
185 fn default() -> Self {
186 Self {
187 services: Arc::new(HashMap::new()),
188 sessions: HashSet::new(),
189 next_session: 0,
190 }
191 }
192}
193
194#[derive(Clone)]
196pub struct Relay<T> {
197 namespace: String,
198 kind: TransportKind,
199 config: HashMap<String, T>,
200}
201
202impl<T> Relay<T> {
203 pub fn tcp(config: HashMap<String, T>) -> Self {
205 Self {
206 namespace: TCP.to_string(),
207 kind: TransportKind::Tcp,
208 config,
209 }
210 }
211
212 pub fn udp(config: HashMap<String, T>) -> Self {
214 Self {
215 namespace: UDP.to_string(),
216 kind: TransportKind::Udp,
217 config,
218 }
219 }
220}
221
222impl<T> Protocol for Relay<T>
223where T: Clone + DeserializeOwned + Serialize + MaybeSend + 'static
224{
225 type State = RelayState<T>;
226 type Event = RelayEvent<T>;
227 type Effect = RelayEffect<T>;
228
229 fn namespace(&self) -> &str {
230 self.namespace.as_str()
231 }
232
233 fn init(&self) -> RelayState<T> {
234 RelayState {
235 services: Arc::new(self.config.clone()),
236 sessions: HashSet::new(),
237 next_session: 0,
238 }
239 }
240
241 fn decode(&self, wire: Wire<'_>) -> Result<RelayEvent<T>, Reject> {
242 if wire.from == wire.me {
243 let command = bincode::deserialize::<RelayCommand<T>>(wire.payload)
244 .map_err(|e| Reject(format!("bad relay command: {e}")))?;
245 Ok(RelayEvent::Command(command))
246 } else {
247 let frame = bincode::deserialize::<Frame>(wire.payload)
248 .map_err(|e| Reject(format!("bad relay frame: {e}")))?;
249 Ok(RelayEvent::Frame {
250 from: wire.from,
251 frame,
252 })
253 }
254 }
255
256 fn step(
257 &self,
258 ctx: Ctx<'_, RelayState<T>>,
259 event: RelayEvent<T>,
260 ) -> Transition<RelayState<T>, RelayEffect<T>> {
261 match event {
262 RelayEvent::Command(command) => {
263 step_command(self.namespace.as_str(), ctx.state, command)
264 }
265 RelayEvent::Frame { from, frame } => {
266 step_frame(self.kind, self.namespace.as_str(), ctx.state, from, frame)
267 }
268 }
269 }
270}
271
272fn step_command<T: Clone>(
277 namespace: &str,
278 state: &RelayState<T>,
279 command: RelayCommand<T>,
280) -> Transition<RelayState<T>, RelayEffect<T>> {
281 let mut next = state.clone();
282 match command {
283 RelayCommand::RegisterService { name, target } => {
284 Arc::make_mut(&mut next.services).insert(name, target);
285 Transition::pure(next)
286 }
287 RelayCommand::UnregisterService { name } => {
288 Arc::make_mut(&mut next.services).remove(&name);
289 Transition::pure(next)
290 }
291 RelayCommand::Accepted {
292 token,
293 peer,
294 service,
295 } => {
296 let session = SessionId(next.next_session);
299 next.next_session += 1;
300 let key = SessionKey::new(peer, namespace, session, Initiator::Local);
302 next.sessions.insert(key.clone());
303 Transition::with(next, vec![RelayEffect::OpenAccepted {
304 token,
305 key,
306 service,
307 }])
308 }
309 RelayCommand::Untrack {
310 peer,
311 session,
312 initiator,
313 } => {
314 next.sessions
315 .remove(&SessionKey::new(peer, namespace, session, initiator));
316 Transition::pure(next)
317 }
318 }
319}
320
321fn step_frame<T: Clone>(
323 kind: TransportKind,
324 namespace: &str,
325 state: &RelayState<T>,
326 from: Did,
327 frame: Frame,
328) -> Transition<RelayState<T>, RelayEffect<T>> {
329 match frame {
330 Frame::Open { session, service } => {
332 let key = SessionKey::new(from, namespace, session, Initiator::Remote);
333 if state.sessions.contains(&key) {
335 return Transition::pure(state.clone());
336 }
337 match state.services.get(service.as_str()) {
338 Some(target) => {
339 let target = target.clone();
340 let mut next = state.clone();
341 next.sessions.insert(key.clone());
342 Transition::with(next, vec![RelayEffect::Connect { key, target, kind }])
343 }
344 None => Transition::with(state.clone(), vec![RelayEffect::SendClose {
345 to: from,
346 session,
347 from_opener: false,
349 }]),
350 }
351 }
352 Frame::Data {
356 session,
357 from_opener,
358 bytes,
359 } => {
360 let key = SessionKey::new(from, namespace, session, opener_to_initiator(from_opener));
361 if state.sessions.contains(&key) {
362 Transition::with(state.clone(), vec![RelayEffect::Write { key, bytes }])
363 } else {
364 Transition::pure(state.clone())
365 }
366 }
367 Frame::Shutdown {
368 session,
369 from_opener,
370 } => {
371 let key = SessionKey::new(from, namespace, session, opener_to_initiator(from_opener));
372 if state.sessions.contains(&key) {
373 Transition::with(state.clone(), vec![RelayEffect::Shutdown { key }])
374 } else {
375 Transition::pure(state.clone())
376 }
377 }
378 Frame::Close {
379 session,
380 from_opener,
381 } => {
382 let key = SessionKey::new(from, namespace, session, opener_to_initiator(from_opener));
383 if state.sessions.contains(&key) {
384 let mut next = state.clone();
385 next.sessions.remove(&key);
386 Transition::with(next, vec![RelayEffect::Close { key }])
387 } else {
388 Transition::pure(state.clone())
389 }
390 }
391 }
392}
393
394fn opener_to_initiator(from_opener: bool) -> Initiator {
396 if from_opener {
397 Initiator::Remote
398 } else {
399 Initiator::Local
400 }
401}
402
403pub(crate) fn close_frame(session: SessionId, from_opener: bool) -> Bytes {
406 let frame = Frame::Close {
407 session,
408 from_opener,
409 };
410 Bytes::from(bincode::serialize(&frame).unwrap_or_default())
411}
412
413#[cfg(feature = "node")]
419pub(crate) struct NativeRelay {
420 engine: Arc<crate::extension::transport::engine::TransportSessions>,
421}
422
423#[cfg(feature = "node")]
424impl NativeRelay {
425 pub(crate) fn new(engine: Arc<crate::extension::transport::engine::TransportSessions>) -> Self {
427 Self { engine }
428 }
429}
430
431#[cfg(feature = "node")]
432#[async_trait::async_trait]
433impl Interpret for NativeRelay {
434 type Effect = RelayEffect<std::net::SocketAddr>;
435
436 async fn run(
437 &self,
438 scope: &Scope,
439 effect: RelayEffect<std::net::SocketAddr>,
440 ) -> crate::error::Result<Vec<Bytes>> {
441 match effect {
442 RelayEffect::Connect { key, target, kind } => {
443 self.engine
444 .clone()
445 .connect(scope.clone(), key, target, kind)
446 .await;
447 }
448 RelayEffect::Write { key, bytes } => {
449 self.engine.write(&key, bytes).await;
450 }
451 RelayEffect::Shutdown { key } => {
452 self.engine.shutdown(&key).await;
453 }
454 RelayEffect::Close { key } => {
455 self.engine.close(scope, &key).await;
456 }
457 RelayEffect::SendClose {
458 to,
459 session,
460 from_opener,
461 } => {
462 scope.send(to, close_frame(session, from_opener)).await?;
463 }
464 RelayEffect::OpenAccepted {
465 token,
466 key,
467 service,
468 } => {
469 self.engine
470 .clone()
471 .bind_accepted(scope.clone(), token, key, service)
472 .await;
473 }
474 }
475 Ok(Vec::new())
476 }
477}
478
479#[cfg(feature = "browser")]
483pub(crate) struct WtRelay {
484 engine: Arc<crate::extension::transport::wt::WtSessions>,
485}
486
487#[cfg(feature = "browser")]
488impl WtRelay {
489 pub(crate) fn new(engine: Arc<crate::extension::transport::wt::WtSessions>) -> Self {
491 Self { engine }
492 }
493}
494
495#[cfg(feature = "browser")]
496#[async_trait::async_trait(?Send)]
497impl Interpret for WtRelay {
498 type Effect = RelayEffect<String>;
499
500 async fn run(
501 &self,
502 scope: &Scope,
503 effect: RelayEffect<String>,
504 ) -> crate::error::Result<Vec<Bytes>> {
505 match effect {
506 RelayEffect::Connect { key, target, kind } => {
507 self.engine
508 .clone()
509 .connect(scope.clone(), key, target, kind)
510 .await;
511 }
512 RelayEffect::Write { key, bytes } => {
513 self.engine.write(&key, bytes).await;
514 }
515 RelayEffect::Shutdown { key } => {
516 self.engine.shutdown(&key).await;
517 }
518 RelayEffect::Close { key } => {
519 self.engine.close(scope, &key).await;
520 }
521 RelayEffect::SendClose {
522 to,
523 session,
524 from_opener,
525 } => {
526 scope.send(to, close_frame(session, from_opener)).await?;
527 }
528 RelayEffect::OpenAccepted { .. } => {
531 tracing::warn!("browser relay received OpenAccepted; it has no local listener");
532 }
533 }
534 Ok(Vec::new())
535 }
536}
537
538#[cfg(feature = "node")]
548#[derive(Clone)]
549pub struct RelayHandle {
550 engine: Arc<crate::extension::transport::engine::TransportSessions>,
551 tcp: Scope,
552 udp: Scope,
553}
554
555#[cfg(feature = "node")]
556impl RelayHandle {
557 pub fn install(extensions: &crate::extension::ext::Extensions) -> crate::error::Result<Self> {
562 let engine = Arc::new(crate::extension::transport::engine::TransportSessions::new());
563 extensions.register_many(vec![
565 (Relay::tcp(HashMap::new()), NativeRelay::new(engine.clone())),
566 (Relay::udp(HashMap::new()), NativeRelay::new(engine.clone())),
567 ])?;
568 let core = extensions.core();
569 Ok(Self {
570 engine,
571 tcp: Scope::new(core.clone(), TCP.to_string()),
572 udp: Scope::new(core, UDP.to_string()),
573 })
574 }
575
576 pub async fn open_tcp_tunnel(
579 &self,
580 local_addr: std::net::SocketAddr,
581 peer: Did,
582 service: String,
583 ) -> crate::error::Result<()> {
584 self.open_tunnel(&self.tcp, local_addr, peer, service, TransportKind::Tcp)
585 .await
586 }
587
588 pub async fn open_udp_tunnel(
591 &self,
592 local_addr: std::net::SocketAddr,
593 peer: Did,
594 service: String,
595 ) -> crate::error::Result<()> {
596 self.open_tunnel(&self.udp, local_addr, peer, service, TransportKind::Udp)
597 .await
598 }
599
600 async fn open_tunnel(
601 &self,
602 scope: &Scope,
603 local_addr: std::net::SocketAddr,
604 peer: Did,
605 service: String,
606 kind: TransportKind,
607 ) -> crate::error::Result<()> {
608 self.engine
612 .clone()
613 .listen(scope.clone(), local_addr, peer, service, kind)
614 .await;
615 Ok(())
616 }
617
618 pub async fn register_tcp_service(
620 &self,
621 name: String,
622 addr: std::net::SocketAddr,
623 ) -> crate::error::Result<()> {
624 register_service(&self.tcp, name, addr).await
625 }
626
627 pub async fn register_udp_service(
629 &self,
630 name: String,
631 addr: std::net::SocketAddr,
632 ) -> crate::error::Result<()> {
633 register_service(&self.udp, name, addr).await
634 }
635}
636
637#[cfg(any(feature = "node", feature = "browser"))]
640async fn register_service<T>(scope: &Scope, name: String, target: T) -> crate::error::Result<()>
641where T: Serialize {
642 let command = RelayCommand::RegisterService { name, target };
643 let payload = bincode::serialize(&command).map_err(|_| crate::error::Error::EncodeError)?;
644 scope.inject(Bytes::from(payload)).await
645}
646
647#[cfg(feature = "browser")]
654#[derive(Clone)]
655pub struct RelayHandle {
656 tcp: Scope,
657 udp: Scope,
658}
659
660#[cfg(feature = "browser")]
661impl RelayHandle {
662 pub fn install(extensions: &crate::extension::ext::Extensions) -> crate::error::Result<Self> {
672 let engine = Arc::new(crate::extension::transport::wt::WtSessions::new());
673 extensions.register_many(vec![
675 (Relay::tcp(HashMap::new()), WtRelay::new(engine.clone())),
676 (Relay::udp(HashMap::new()), WtRelay::new(engine)),
677 ])?;
678 let core = extensions.core();
679 Ok(Self {
680 tcp: Scope::new(core.clone(), TCP.to_string()),
681 udp: Scope::new(core, UDP.to_string()),
682 })
683 }
684
685 pub async fn register_wt_service(&self, name: String, url: String) -> crate::error::Result<()> {
688 register_service(&self.tcp, name, url).await
689 }
690
691 pub async fn register_wt_udp_service(
694 &self,
695 name: String,
696 url: String,
697 ) -> crate::error::Result<()> {
698 register_service(&self.udp, name, url).await
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use std::collections::HashMap;
705 use std::collections::HashSet;
706 use std::net::SocketAddr;
707
708 use bytes::Bytes;
709 use rings_core::dht::Did;
710
711 use super::Frame;
712 use super::Initiator;
713 use super::Relay;
714 use super::RelayCommand;
715 use super::RelayEffect;
716 use super::RelayState;
717 use super::SessionId;
718 use super::SessionKey;
719 use super::TransportKind;
720 use crate::extension::ext::Ctx;
721 use crate::extension::ext::Protocol;
722 use crate::extension::ext::Transition;
723 use crate::extension::ext::Wire;
724
725 fn this_node() -> Did {
726 Did::from(1u32)
727 }
728 fn peer_a() -> Did {
729 Did::from(2u32)
730 }
731 fn peer_b() -> Did {
732 Did::from(3u32)
733 }
734 fn web_addr() -> SocketAddr {
735 "127.0.0.1:8080".parse().unwrap()
736 }
737
738 fn rkey(peer: Did, session: u64) -> SessionKey {
740 SessionKey::new(peer, super::TCP, SessionId(session), Initiator::Remote)
741 }
742 fn data(session: u64, bytes: &'static [u8]) -> Frame {
744 Frame::Data {
745 session: SessionId(session),
746 from_opener: true,
747 bytes: Bytes::from_static(bytes),
748 }
749 }
750 fn close(session: u64) -> Frame {
752 Frame::Close {
753 session: SessionId(session),
754 from_opener: true,
755 }
756 }
757 fn open(session: u64, service: &str) -> Frame {
759 Frame::Open {
760 session: SessionId(session),
761 service: service.to_string(),
762 }
763 }
764
765 fn web_relay() -> Relay<SocketAddr> {
766 let mut config = HashMap::new();
767 config.insert("web".to_string(), web_addr());
768 Relay::tcp(config)
769 }
770
771 fn step_frame(
773 relay: &Relay<SocketAddr>,
774 state: &RelayState<SocketAddr>,
775 from: Did,
776 frame: &Frame,
777 ) -> Transition<RelayState<SocketAddr>, RelayEffect<SocketAddr>> {
778 let payload = bincode::serialize(frame).unwrap();
779 let event = relay
780 .decode(Wire {
781 from,
782 me: this_node(),
783 payload: payload.as_ref(),
784 })
785 .unwrap();
786 relay.step(
787 Ctx {
788 did: this_node(),
789 state,
790 },
791 event,
792 )
793 }
794
795 fn step_command(
797 relay: &Relay<SocketAddr>,
798 state: &RelayState<SocketAddr>,
799 command: &RelayCommand<SocketAddr>,
800 ) -> Transition<RelayState<SocketAddr>, RelayEffect<SocketAddr>> {
801 let payload = bincode::serialize(command).unwrap();
802 let event = relay
803 .decode(Wire {
804 from: this_node(),
805 me: this_node(),
806 payload: payload.as_ref(),
807 })
808 .unwrap();
809 relay.step(
810 Ctx {
811 did: this_node(),
812 state,
813 },
814 event,
815 )
816 }
817
818 #[test]
819 fn open_known_service_connects_and_records_the_session() {
820 let relay = web_relay();
821 let t = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
822 let expected = rkey(peer_a(), 7);
823 match t.effects.as_slice() {
824 [RelayEffect::Connect { key, target, kind }] => {
825 assert_eq!(*key, expected);
826 assert_eq!(*target, web_addr());
827 assert!(matches!(kind, TransportKind::Tcp));
828 }
829 other => panic!("expected one Connect, got {other:?}"),
830 }
831 assert!(t.state.sessions.contains(&expected));
832 }
833
834 #[test]
835 fn duplicate_open_for_a_live_session_is_rejected() {
836 let relay = web_relay();
837 let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
838 assert!(opened.state.sessions.contains(&rkey(peer_a(), 7)));
839 let again = step_frame(&relay, &opened.state, peer_a(), &open(7, "web"));
840 assert!(
841 again.effects.is_empty(),
842 "duplicate Open must emit no effect"
843 );
844 assert_eq!(again.state.sessions.len(), 1);
845 }
846
847 #[test]
848 fn open_unknown_service_closes_and_records_nothing() {
849 let relay = web_relay();
850 let t = step_frame(&relay, &relay.init(), peer_a(), &open(7, "ssh"));
851 match t.effects.as_slice() {
852 [RelayEffect::SendClose {
853 to,
854 session,
855 from_opener,
856 }] => {
857 assert_eq!(*to, peer_a());
858 assert_eq!(*session, SessionId(7));
859 assert!(!from_opener, "we are not the opener of the peer's session");
860 }
861 other => panic!("expected one SendClose, got {other:?}"),
862 }
863 assert!(t.state.sessions.is_empty());
864 }
865
866 #[test]
867 fn data_writes_to_a_live_keyed_session() {
868 let relay = web_relay();
869 let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
871 let t = step_frame(&relay, &opened.state, peer_a(), &data(7, b"hello"));
872 match t.effects.as_slice() {
873 [RelayEffect::Write { key, bytes }] => {
874 assert_eq!(*key, rkey(peer_a(), 7));
875 assert_eq!(bytes.as_ref(), b"hello");
876 }
877 other => panic!("expected one Write, got {other:?}"),
878 }
879 }
880
881 #[test]
882 fn data_for_an_unknown_session_is_dropped_by_the_reducer() {
883 let relay = web_relay();
884 let t = step_frame(&relay, &relay.init(), peer_a(), &data(7, b"hello"));
886 assert!(
887 t.effects.is_empty(),
888 "Data for an unknown session emits nothing"
889 );
890 }
891
892 #[test]
893 fn close_removes_the_session_and_emits_close() {
894 let relay = web_relay();
895 let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
896 let t = step_frame(&relay, &opened.state, peer_a(), &close(7));
897 let expected = rkey(peer_a(), 7);
898 match t.effects.as_slice() {
899 [RelayEffect::Close { key }] => assert_eq!(*key, expected),
900 other => panic!("expected one Close, got {other:?}"),
901 }
902 assert!(!t.state.sessions.contains(&expected));
903 }
904
905 #[test]
906 fn register_service_via_self_command_then_open_connects() {
907 let relay = Relay::tcp(HashMap::new());
908 let registered = step_command(&relay, &relay.init(), &RelayCommand::RegisterService {
909 name: "web".to_string(),
910 target: web_addr(),
911 });
912 assert!(registered.effects.is_empty());
913 let t = step_frame(&relay, ®istered.state, peer_a(), &open(1, "web"));
914 match t.effects.as_slice() {
915 [RelayEffect::Connect { target, .. }] => assert_eq!(*target, web_addr()),
916 other => panic!("expected one Connect, got {other:?}"),
917 }
918 }
919
920 #[test]
921 fn accepted_mints_in_the_core_then_untrack_removes() {
922 let relay = web_relay();
923 let accepted = step_command(&relay, &relay.init(), &RelayCommand::Accepted {
926 token: 42,
927 peer: peer_a(),
928 service: "web".to_string(),
929 });
930 let key = SessionKey::new(peer_a(), super::TCP, SessionId(0), Initiator::Local);
931 match accepted.effects.as_slice() {
932 [RelayEffect::OpenAccepted {
933 token,
934 key: k,
935 service,
936 }] => {
937 assert_eq!(*token, 42);
938 assert_eq!(*k, key);
939 assert_eq!(service, "web");
940 }
941 other => panic!("expected one OpenAccepted, got {other:?}"),
942 }
943 assert!(accepted.state.sessions.contains(&key));
944
945 let untracked = step_command(&relay, &accepted.state, &RelayCommand::Untrack {
946 peer: peer_a(),
947 session: SessionId(0),
948 initiator: Initiator::Local,
949 });
950 assert!(untracked.effects.is_empty());
951 assert!(!untracked.state.sessions.contains(&key));
952 }
953
954 #[test]
955 fn a_peer_cannot_address_another_peers_session() {
956 let relay = web_relay();
957 let a_open = step_frame(&relay, &relay.init(), peer_a(), &open(0, "web"));
958 let key_a = rkey(peer_a(), 0);
959 assert!(a_open.state.sessions.contains(&key_a));
960
961 let b_data = step_frame(&relay, &a_open.state, peer_b(), &data(0, b"x"));
964 assert!(
965 b_data.effects.is_empty(),
966 "B's Data for a session it did not open is dropped"
967 );
968 let b_close = step_frame(&relay, &a_open.state, peer_b(), &close(0));
969 assert!(
970 b_close.effects.is_empty(),
971 "B's Close for a session it did not open is dropped"
972 );
973 assert!(b_close.state.sessions.contains(&key_a));
974 }
975
976 #[test]
977 fn local_and_remote_sessions_with_the_same_id_do_not_collide() {
978 let relay = web_relay();
981 let opened = step_frame(&relay, &relay.init(), peer_a(), &open(0, "web"));
982 let accepted = step_command(&relay, &opened.state, &RelayCommand::Accepted {
983 token: 1,
984 peer: peer_a(),
985 service: "web".to_string(),
986 });
987 let remote = SessionKey::new(peer_a(), super::TCP, SessionId(0), Initiator::Remote);
988 let local = SessionKey::new(peer_a(), super::TCP, SessionId(0), Initiator::Local);
989 assert_ne!(remote, local);
990 assert!(accepted.state.sessions.contains(&remote));
991 assert!(accepted.state.sessions.contains(&local));
992 assert_eq!(accepted.state.sessions.len(), 2);
993 }
994
995 #[test]
998 fn open_then_close_then_data_does_not_resurrect_the_session() {
999 let relay = web_relay();
1000 let key = rkey(peer_a(), 3);
1001 let opened = step_frame(&relay, &relay.init(), peer_a(), &open(3, "web"));
1002 assert!(opened.state.sessions.contains(&key));
1003 let closed = step_frame(&relay, &opened.state, peer_a(), &close(3));
1004 assert!(!closed.state.sessions.contains(&key));
1005 let late = step_frame(&relay, &closed.state, peer_a(), &data(3, b"late"));
1008 assert!(late.effects.is_empty());
1009 assert!(late.state.sessions.is_empty());
1010 }
1011
1012 #[test]
1013 fn close_after_close_is_idempotent() {
1014 let relay = web_relay();
1015 let opened = step_frame(&relay, &relay.init(), peer_a(), &open(5, "web"));
1016 let c1 = step_frame(&relay, &opened.state, peer_a(), &close(5));
1017 assert!(matches!(c1.effects.as_slice(), [RelayEffect::Close { .. }]));
1018 let c2 = step_frame(&relay, &c1.state, peer_a(), &close(5));
1020 assert!(c2.effects.is_empty());
1021 assert!(c2.state.sessions.is_empty());
1022 }
1023
1024 #[test]
1025 fn malformed_payload_is_rejected_at_the_boundary() {
1026 let relay = web_relay();
1027 let bad = [0xFFu8, 0xFF, 0xFF, 0xFF, 0xFF];
1028 let result = relay.decode(Wire {
1029 from: peer_a(),
1030 me: this_node(),
1031 payload: &bad,
1032 });
1033 assert!(result.is_err(), "a malformed frame must be rejected");
1034 }
1035
1036 #[test]
1040 fn lifecycle_property_state_never_diverges_from_model() {
1041 let relay = web_relay();
1042 let peers = [peer_a(), peer_b(), Did::from(4u32)];
1043 let mut state = relay.init();
1044 let mut model: HashSet<SessionKey> = HashSet::new();
1045 let mut rng: u64 = 0x2545_F491_4F6C_DD1D;
1046 let mut next = move || {
1047 rng ^= rng << 13;
1048 rng ^= rng >> 7;
1049 rng ^= rng << 17;
1050 rng
1051 };
1052
1053 for _ in 0..4000 {
1054 let r = next();
1055 let peer = peers[(r % 3) as usize];
1056 let session = (r >> 2) & 0x7; let key = rkey(peer, session);
1058 let transition = match (r >> 8) % 4 {
1059 0 => {
1060 let t = step_frame(&relay, &state, peer, &open(session, "web"));
1061 if model.contains(&key) {
1062 assert!(t.effects.is_empty(), "duplicate Open must emit nothing");
1063 } else {
1064 assert!(matches!(t.effects.as_slice(), [
1065 RelayEffect::Connect { .. }
1066 ]));
1067 model.insert(key.clone());
1068 }
1069 t
1070 }
1071 1 => {
1072 let t = step_frame(&relay, &state, peer, &open(session, "nope"));
1073 if model.contains(&key) {
1074 assert!(t.effects.is_empty());
1075 } else {
1076 assert!(matches!(t.effects.as_slice(), [
1077 RelayEffect::SendClose { .. }
1078 ]));
1079 }
1080 t
1081 }
1082 2 => {
1083 let t = step_frame(&relay, &state, peer, &data(session, b"x"));
1084 if model.contains(&key) {
1085 match t.effects.as_slice() {
1086 [RelayEffect::Write { key: k, .. }] => assert_eq!(*k, key),
1087 other => panic!("expected one Write, got {other:?}"),
1088 }
1089 } else {
1090 assert!(
1091 t.effects.is_empty(),
1092 "Data on an unknown session is dropped"
1093 );
1094 }
1095 t
1096 }
1097 _ => {
1098 let t = step_frame(&relay, &state, peer, &close(session));
1099 if model.contains(&key) {
1100 assert!(matches!(t.effects.as_slice(), [RelayEffect::Close { .. }]));
1101 } else {
1102 assert!(
1103 t.effects.is_empty(),
1104 "Close on an unknown session is dropped"
1105 );
1106 }
1107 model.remove(&key);
1108 t
1109 }
1110 };
1111 state = transition.state;
1112 assert_eq!(
1113 state.sessions, model,
1114 "State.sessions diverged from the model"
1115 );
1116 }
1117 }
1118
1119 struct EngineModel {
1125 map: HashMap<SessionKey, u64>,
1126 next_gen: u64,
1127 }
1128
1129 impl EngineModel {
1130 fn new() -> Self {
1131 Self {
1132 map: HashMap::new(),
1133 next_gen: 0,
1134 }
1135 }
1136 fn register(&mut self, key: SessionKey) -> u64 {
1137 let gen = self.next_gen;
1138 self.next_gen += 1;
1139 self.map.insert(key, gen);
1140 gen
1141 }
1142 fn close(&mut self, key: &SessionKey) {
1143 self.map.remove(key);
1144 }
1145 fn close_if_current(&mut self, key: &SessionKey, gen: u64) -> bool {
1148 if self.map.get(key) == Some(&gen) {
1149 self.map.remove(key);
1150 true
1151 } else {
1152 false
1153 }
1154 }
1155 }
1156
1157 fn apply_effects(
1161 eng: &mut EngineModel,
1162 effects: &[RelayEffect<SocketAddr>],
1163 ) -> Option<(SessionKey, u64)> {
1164 let mut registered = None;
1165 for effect in effects {
1166 match effect {
1167 RelayEffect::Connect { key, .. } | RelayEffect::OpenAccepted { key, .. } => {
1168 registered = Some((key.clone(), eng.register(key.clone())));
1169 }
1170 RelayEffect::Write { key, .. } | RelayEffect::Shutdown { key } => {
1171 assert!(
1172 eng.map.contains_key(key),
1173 "effect targeted a non-live session"
1174 );
1175 }
1176 RelayEffect::Close { key } => eng.close(key),
1177 RelayEffect::SendClose { .. } => {}
1178 }
1179 }
1180 registered
1181 }
1182
1183 #[test]
1184 fn generation_prevents_a_slow_old_task_deleting_a_reopened_handle() {
1185 let relay = web_relay();
1189 let mut eng = EngineModel::new();
1190
1191 let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
1192 let (key, gen_old) = apply_effects(&mut eng, &opened.effects).expect("registered");
1193
1194 let closed = step_frame(&relay, &opened.state, peer_a(), &close(7));
1196 apply_effects(&mut eng, &closed.effects);
1197 assert!(!eng.map.contains_key(&key));
1198
1199 let reopened = step_frame(&relay, &closed.state, peer_a(), &open(7, "web"));
1201 let (_, gen_new) = apply_effects(&mut eng, &reopened.effects).expect("registered");
1202 assert_ne!(gen_old, gen_new);
1203
1204 let removed = eng.close_if_current(&key, gen_old);
1207 assert!(
1208 !removed,
1209 "stale task must not remove — and so must send no peer Close"
1210 );
1211 assert_eq!(
1212 eng.map.get(&key),
1213 Some(&gen_new),
1214 "old task must not delete the reopened handle"
1215 );
1216 }
1217
1218 #[test]
1219 fn engine_model_stays_consistent_with_step_under_interleaving() {
1220 let relay = web_relay();
1223 let peers = [peer_a(), peer_b()];
1224 let mut state = relay.init();
1225 let mut eng = EngineModel::new();
1226 let mut tasks: Vec<(SessionKey, u64)> = Vec::new();
1227 let mut rng: u64 = 0x9E37_79B9_7F4A_7C15;
1228 let mut next = move || {
1229 rng ^= rng << 13;
1230 rng ^= rng >> 7;
1231 rng ^= rng << 17;
1232 rng
1233 };
1234
1235 for _ in 0..3000 {
1236 let r = next();
1237 let peer = peers[(r % 2) as usize];
1238 let session = (r >> 2) & 0x3; match (r >> 8) % 4 {
1240 0 => {
1241 let t = step_frame(&relay, &state, peer, &open(session, "web"));
1242 if let Some(task) = apply_effects(&mut eng, &t.effects) {
1243 tasks.push(task);
1244 }
1245 state = t.state;
1246 }
1247 1 => {
1248 let t = step_frame(&relay, &state, peer, &data(session, b"x"));
1249 apply_effects(&mut eng, &t.effects);
1250 state = t.state;
1251 }
1252 2 => {
1253 let t = step_frame(&relay, &state, peer, &close(session));
1256 apply_effects(&mut eng, &t.effects);
1257 state = t.state;
1258 }
1259 _ => {
1260 if !tasks.is_empty() {
1265 let idx = (r >> 16) as usize % tasks.len();
1266 let (tkey, tgen) = tasks.swap_remove(idx);
1267 let removed = eng.close_if_current(&tkey, tgen);
1268 if removed {
1269 let untrack = RelayCommand::Untrack {
1270 peer: tkey.peer,
1271 session: tkey.session,
1272 initiator: tkey.initiator,
1273 };
1274 state = step_command(&relay, &state, &untrack).state;
1275 }
1276 }
1277 }
1278 }
1279 let live: HashSet<SessionKey> = eng.map.keys().cloned().collect();
1281 assert_eq!(
1282 state.sessions, live,
1283 "pure state diverged from the engine model"
1284 );
1285 }
1286 }
1287}