#![warn(missing_docs)]
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use bytes::Bytes;
use rings_core::dht::Did;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::Serialize;
use crate::extension::ext::Ctx;
use crate::extension::ext::Interpret;
use crate::extension::ext::MaybeSend;
use crate::extension::ext::Protocol;
use crate::extension::ext::Reject;
use crate::extension::ext::Scope;
use crate::extension::ext::Transition;
use crate::extension::ext::Wire;
use crate::extension::transport::Frame;
use crate::extension::transport::Initiator;
use crate::extension::transport::SessionId;
use crate::extension::transport::SessionKey;
use crate::extension::transport::TransportKind;
pub const TCP: &str = "tcp";
pub const UDP: &str = "udp";
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum RelayCommand<T> {
RegisterService {
name: String,
target: T,
},
UnregisterService {
name: String,
},
Accepted {
token: u64,
peer: Did,
service: String,
},
Untrack {
peer: Did,
session: SessionId,
initiator: Initiator,
},
}
pub enum RelayEvent<T> {
Command(RelayCommand<T>),
Frame {
from: Did,
frame: Frame,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RelayEffect<T> {
Connect {
key: SessionKey,
target: T,
kind: TransportKind,
},
Write {
key: SessionKey,
bytes: Bytes,
},
Shutdown {
key: SessionKey,
},
Close {
key: SessionKey,
},
SendClose {
to: Did,
session: SessionId,
from_opener: bool,
},
OpenAccepted {
token: u64,
key: SessionKey,
service: String,
},
}
#[derive(Clone)]
pub struct RelayState<T> {
services: Arc<HashMap<String, T>>,
sessions: HashSet<SessionKey>,
next_session: u64,
}
impl<T> Default for RelayState<T> {
fn default() -> Self {
Self {
services: Arc::new(HashMap::new()),
sessions: HashSet::new(),
next_session: 0,
}
}
}
#[derive(Clone)]
pub struct Relay<T> {
namespace: String,
kind: TransportKind,
config: HashMap<String, T>,
}
impl<T> Relay<T> {
pub fn tcp(config: HashMap<String, T>) -> Self {
Self {
namespace: TCP.to_string(),
kind: TransportKind::Tcp,
config,
}
}
pub fn udp(config: HashMap<String, T>) -> Self {
Self {
namespace: UDP.to_string(),
kind: TransportKind::Udp,
config,
}
}
}
impl<T> Protocol for Relay<T>
where T: Clone + DeserializeOwned + Serialize + MaybeSend + 'static
{
type State = RelayState<T>;
type Event = RelayEvent<T>;
type Effect = RelayEffect<T>;
fn namespace(&self) -> &str {
self.namespace.as_str()
}
fn init(&self) -> RelayState<T> {
RelayState {
services: Arc::new(self.config.clone()),
sessions: HashSet::new(),
next_session: 0,
}
}
fn decode(&self, wire: Wire<'_>) -> Result<RelayEvent<T>, Reject> {
if wire.from == wire.me {
let command = bincode::deserialize::<RelayCommand<T>>(wire.payload)
.map_err(|e| Reject(format!("bad relay command: {e}")))?;
Ok(RelayEvent::Command(command))
} else {
let frame = bincode::deserialize::<Frame>(wire.payload)
.map_err(|e| Reject(format!("bad relay frame: {e}")))?;
Ok(RelayEvent::Frame {
from: wire.from,
frame,
})
}
}
fn step(
&self,
ctx: Ctx<'_, RelayState<T>>,
event: RelayEvent<T>,
) -> Transition<RelayState<T>, RelayEffect<T>> {
match event {
RelayEvent::Command(command) => {
step_command(self.namespace.as_str(), ctx.state, command)
}
RelayEvent::Frame { from, frame } => {
step_frame(self.kind, self.namespace.as_str(), ctx.state, from, frame)
}
}
}
}
fn step_command<T: Clone>(
namespace: &str,
state: &RelayState<T>,
command: RelayCommand<T>,
) -> Transition<RelayState<T>, RelayEffect<T>> {
let mut next = state.clone();
match command {
RelayCommand::RegisterService { name, target } => {
Arc::make_mut(&mut next.services).insert(name, target);
Transition::pure(next)
}
RelayCommand::UnregisterService { name } => {
Arc::make_mut(&mut next.services).remove(&name);
Transition::pure(next)
}
RelayCommand::Accepted {
token,
peer,
service,
} => {
let session = SessionId(next.next_session);
next.next_session += 1;
let key = SessionKey::new(peer, namespace, session, Initiator::Local);
next.sessions.insert(key.clone());
Transition::with(next, vec![RelayEffect::OpenAccepted {
token,
key,
service,
}])
}
RelayCommand::Untrack {
peer,
session,
initiator,
} => {
next.sessions
.remove(&SessionKey::new(peer, namespace, session, initiator));
Transition::pure(next)
}
}
}
fn step_frame<T: Clone>(
kind: TransportKind,
namespace: &str,
state: &RelayState<T>,
from: Did,
frame: Frame,
) -> Transition<RelayState<T>, RelayEffect<T>> {
match frame {
Frame::Open { session, service } => {
let key = SessionKey::new(from, namespace, session, Initiator::Remote);
if state.sessions.contains(&key) {
return Transition::pure(state.clone());
}
match state.services.get(service.as_str()) {
Some(target) => {
let target = target.clone();
let mut next = state.clone();
next.sessions.insert(key.clone());
Transition::with(next, vec![RelayEffect::Connect { key, target, kind }])
}
None => Transition::with(state.clone(), vec![RelayEffect::SendClose {
to: from,
session,
from_opener: false,
}]),
}
}
Frame::Data {
session,
from_opener,
bytes,
} => {
let key = SessionKey::new(from, namespace, session, opener_to_initiator(from_opener));
if state.sessions.contains(&key) {
Transition::with(state.clone(), vec![RelayEffect::Write { key, bytes }])
} else {
Transition::pure(state.clone())
}
}
Frame::Shutdown {
session,
from_opener,
} => {
let key = SessionKey::new(from, namespace, session, opener_to_initiator(from_opener));
if state.sessions.contains(&key) {
Transition::with(state.clone(), vec![RelayEffect::Shutdown { key }])
} else {
Transition::pure(state.clone())
}
}
Frame::Close {
session,
from_opener,
} => {
let key = SessionKey::new(from, namespace, session, opener_to_initiator(from_opener));
if state.sessions.contains(&key) {
let mut next = state.clone();
next.sessions.remove(&key);
Transition::with(next, vec![RelayEffect::Close { key }])
} else {
Transition::pure(state.clone())
}
}
}
}
fn opener_to_initiator(from_opener: bool) -> Initiator {
if from_opener {
Initiator::Remote
} else {
Initiator::Local
}
}
pub(crate) fn close_frame(session: SessionId, from_opener: bool) -> Bytes {
let frame = Frame::Close {
session,
from_opener,
};
Bytes::from(bincode::serialize(&frame).unwrap_or_default())
}
#[cfg(feature = "node")]
pub(crate) struct NativeRelay {
engine: Arc<crate::extension::transport::engine::TransportSessions>,
}
#[cfg(feature = "node")]
impl NativeRelay {
pub(crate) fn new(engine: Arc<crate::extension::transport::engine::TransportSessions>) -> Self {
Self { engine }
}
}
#[cfg(feature = "node")]
#[async_trait::async_trait]
impl Interpret for NativeRelay {
type Effect = RelayEffect<std::net::SocketAddr>;
async fn run(
&self,
scope: &Scope,
effect: RelayEffect<std::net::SocketAddr>,
) -> crate::error::Result<Vec<Bytes>> {
match effect {
RelayEffect::Connect { key, target, kind } => {
self.engine
.clone()
.connect(scope.clone(), key, target, kind)
.await;
}
RelayEffect::Write { key, bytes } => {
self.engine.write(&key, bytes).await;
}
RelayEffect::Shutdown { key } => {
self.engine.shutdown(&key).await;
}
RelayEffect::Close { key } => {
self.engine.close(scope, &key).await;
}
RelayEffect::SendClose {
to,
session,
from_opener,
} => {
scope.send(to, close_frame(session, from_opener)).await?;
}
RelayEffect::OpenAccepted {
token,
key,
service,
} => {
self.engine
.clone()
.bind_accepted(scope.clone(), token, key, service)
.await;
}
}
Ok(Vec::new())
}
}
#[cfg(feature = "browser")]
pub(crate) struct WtRelay {
engine: Arc<crate::extension::transport::wt::WtSessions>,
}
#[cfg(feature = "browser")]
impl WtRelay {
pub(crate) fn new(engine: Arc<crate::extension::transport::wt::WtSessions>) -> Self {
Self { engine }
}
}
#[cfg(feature = "browser")]
#[async_trait::async_trait(?Send)]
impl Interpret for WtRelay {
type Effect = RelayEffect<String>;
async fn run(
&self,
scope: &Scope,
effect: RelayEffect<String>,
) -> crate::error::Result<Vec<Bytes>> {
match effect {
RelayEffect::Connect { key, target, kind } => {
self.engine
.clone()
.connect(scope.clone(), key, target, kind)
.await;
}
RelayEffect::Write { key, bytes } => {
self.engine.write(&key, bytes).await;
}
RelayEffect::Shutdown { key } => {
self.engine.shutdown(&key).await;
}
RelayEffect::Close { key } => {
self.engine.close(scope, &key).await;
}
RelayEffect::SendClose {
to,
session,
from_opener,
} => {
scope.send(to, close_frame(session, from_opener)).await?;
}
RelayEffect::OpenAccepted { .. } => {
tracing::warn!("browser relay received OpenAccepted; it has no local listener");
}
}
Ok(Vec::new())
}
}
#[cfg(feature = "node")]
#[derive(Clone)]
pub struct RelayHandle {
engine: Arc<crate::extension::transport::engine::TransportSessions>,
tcp: Scope,
udp: Scope,
}
#[cfg(feature = "node")]
impl RelayHandle {
pub fn install(extensions: &crate::extension::ext::Extensions) -> crate::error::Result<Self> {
let engine = Arc::new(crate::extension::transport::engine::TransportSessions::new());
extensions.register_many(vec![
(Relay::tcp(HashMap::new()), NativeRelay::new(engine.clone())),
(Relay::udp(HashMap::new()), NativeRelay::new(engine.clone())),
])?;
let core = extensions.core();
Ok(Self {
engine,
tcp: Scope::new(core.clone(), TCP.to_string()),
udp: Scope::new(core, UDP.to_string()),
})
}
pub async fn open_tcp_tunnel(
&self,
local_addr: std::net::SocketAddr,
peer: Did,
service: String,
) -> crate::error::Result<()> {
self.open_tunnel(&self.tcp, local_addr, peer, service, TransportKind::Tcp)
.await
}
pub async fn open_udp_tunnel(
&self,
local_addr: std::net::SocketAddr,
peer: Did,
service: String,
) -> crate::error::Result<()> {
self.open_tunnel(&self.udp, local_addr, peer, service, TransportKind::Udp)
.await
}
async fn open_tunnel(
&self,
scope: &Scope,
local_addr: std::net::SocketAddr,
peer: Did,
service: String,
kind: TransportKind,
) -> crate::error::Result<()> {
self.engine
.clone()
.listen(scope.clone(), local_addr, peer, service, kind)
.await;
Ok(())
}
pub async fn register_tcp_service(
&self,
name: String,
addr: std::net::SocketAddr,
) -> crate::error::Result<()> {
register_service(&self.tcp, name, addr).await
}
pub async fn register_udp_service(
&self,
name: String,
addr: std::net::SocketAddr,
) -> crate::error::Result<()> {
register_service(&self.udp, name, addr).await
}
}
#[cfg(any(feature = "node", feature = "browser"))]
async fn register_service<T>(scope: &Scope, name: String, target: T) -> crate::error::Result<()>
where T: Serialize {
let command = RelayCommand::RegisterService { name, target };
let payload = bincode::serialize(&command).map_err(|_| crate::error::Error::EncodeError)?;
scope.inject(Bytes::from(payload)).await
}
#[cfg(feature = "browser")]
#[derive(Clone)]
pub struct RelayHandle {
tcp: Scope,
udp: Scope,
}
#[cfg(feature = "browser")]
impl RelayHandle {
pub fn install(extensions: &crate::extension::ext::Extensions) -> crate::error::Result<Self> {
let engine = Arc::new(crate::extension::transport::wt::WtSessions::new());
extensions.register_many(vec![
(Relay::tcp(HashMap::new()), WtRelay::new(engine.clone())),
(Relay::udp(HashMap::new()), WtRelay::new(engine)),
])?;
let core = extensions.core();
Ok(Self {
tcp: Scope::new(core.clone(), TCP.to_string()),
udp: Scope::new(core, UDP.to_string()),
})
}
pub async fn register_wt_service(&self, name: String, url: String) -> crate::error::Result<()> {
register_service(&self.tcp, name, url).await
}
pub async fn register_wt_udp_service(
&self,
name: String,
url: String,
) -> crate::error::Result<()> {
register_service(&self.udp, name, url).await
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::collections::HashSet;
use std::net::SocketAddr;
use bytes::Bytes;
use rings_core::dht::Did;
use super::Frame;
use super::Initiator;
use super::Relay;
use super::RelayCommand;
use super::RelayEffect;
use super::RelayState;
use super::SessionId;
use super::SessionKey;
use super::TransportKind;
use crate::extension::ext::Ctx;
use crate::extension::ext::Protocol;
use crate::extension::ext::Transition;
use crate::extension::ext::Wire;
fn this_node() -> Did {
Did::from(1u32)
}
fn peer_a() -> Did {
Did::from(2u32)
}
fn peer_b() -> Did {
Did::from(3u32)
}
fn web_addr() -> SocketAddr {
"127.0.0.1:8080".parse().unwrap()
}
fn rkey(peer: Did, session: u64) -> SessionKey {
SessionKey::new(peer, super::TCP, SessionId(session), Initiator::Remote)
}
fn data(session: u64, bytes: &'static [u8]) -> Frame {
Frame::Data {
session: SessionId(session),
from_opener: true,
bytes: Bytes::from_static(bytes),
}
}
fn close(session: u64) -> Frame {
Frame::Close {
session: SessionId(session),
from_opener: true,
}
}
fn open(session: u64, service: &str) -> Frame {
Frame::Open {
session: SessionId(session),
service: service.to_string(),
}
}
fn web_relay() -> Relay<SocketAddr> {
let mut config = HashMap::new();
config.insert("web".to_string(), web_addr());
Relay::tcp(config)
}
fn step_frame(
relay: &Relay<SocketAddr>,
state: &RelayState<SocketAddr>,
from: Did,
frame: &Frame,
) -> Transition<RelayState<SocketAddr>, RelayEffect<SocketAddr>> {
let payload = bincode::serialize(frame).unwrap();
let event = relay
.decode(Wire {
from,
me: this_node(),
payload: payload.as_ref(),
})
.unwrap();
relay.step(
Ctx {
did: this_node(),
state,
},
event,
)
}
fn step_command(
relay: &Relay<SocketAddr>,
state: &RelayState<SocketAddr>,
command: &RelayCommand<SocketAddr>,
) -> Transition<RelayState<SocketAddr>, RelayEffect<SocketAddr>> {
let payload = bincode::serialize(command).unwrap();
let event = relay
.decode(Wire {
from: this_node(),
me: this_node(),
payload: payload.as_ref(),
})
.unwrap();
relay.step(
Ctx {
did: this_node(),
state,
},
event,
)
}
#[test]
fn open_known_service_connects_and_records_the_session() {
let relay = web_relay();
let t = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
let expected = rkey(peer_a(), 7);
match t.effects.as_slice() {
[RelayEffect::Connect { key, target, kind }] => {
assert_eq!(*key, expected);
assert_eq!(*target, web_addr());
assert!(matches!(kind, TransportKind::Tcp));
}
other => panic!("expected one Connect, got {other:?}"),
}
assert!(t.state.sessions.contains(&expected));
}
#[test]
fn duplicate_open_for_a_live_session_is_rejected() {
let relay = web_relay();
let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
assert!(opened.state.sessions.contains(&rkey(peer_a(), 7)));
let again = step_frame(&relay, &opened.state, peer_a(), &open(7, "web"));
assert!(
again.effects.is_empty(),
"duplicate Open must emit no effect"
);
assert_eq!(again.state.sessions.len(), 1);
}
#[test]
fn open_unknown_service_closes_and_records_nothing() {
let relay = web_relay();
let t = step_frame(&relay, &relay.init(), peer_a(), &open(7, "ssh"));
match t.effects.as_slice() {
[RelayEffect::SendClose {
to,
session,
from_opener,
}] => {
assert_eq!(*to, peer_a());
assert_eq!(*session, SessionId(7));
assert!(!from_opener, "we are not the opener of the peer's session");
}
other => panic!("expected one SendClose, got {other:?}"),
}
assert!(t.state.sessions.is_empty());
}
#[test]
fn data_writes_to_a_live_keyed_session() {
let relay = web_relay();
let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
let t = step_frame(&relay, &opened.state, peer_a(), &data(7, b"hello"));
match t.effects.as_slice() {
[RelayEffect::Write { key, bytes }] => {
assert_eq!(*key, rkey(peer_a(), 7));
assert_eq!(bytes.as_ref(), b"hello");
}
other => panic!("expected one Write, got {other:?}"),
}
}
#[test]
fn data_for_an_unknown_session_is_dropped_by_the_reducer() {
let relay = web_relay();
let t = step_frame(&relay, &relay.init(), peer_a(), &data(7, b"hello"));
assert!(
t.effects.is_empty(),
"Data for an unknown session emits nothing"
);
}
#[test]
fn close_removes_the_session_and_emits_close() {
let relay = web_relay();
let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
let t = step_frame(&relay, &opened.state, peer_a(), &close(7));
let expected = rkey(peer_a(), 7);
match t.effects.as_slice() {
[RelayEffect::Close { key }] => assert_eq!(*key, expected),
other => panic!("expected one Close, got {other:?}"),
}
assert!(!t.state.sessions.contains(&expected));
}
#[test]
fn register_service_via_self_command_then_open_connects() {
let relay = Relay::tcp(HashMap::new());
let registered = step_command(&relay, &relay.init(), &RelayCommand::RegisterService {
name: "web".to_string(),
target: web_addr(),
});
assert!(registered.effects.is_empty());
let t = step_frame(&relay, ®istered.state, peer_a(), &open(1, "web"));
match t.effects.as_slice() {
[RelayEffect::Connect { target, .. }] => assert_eq!(*target, web_addr()),
other => panic!("expected one Connect, got {other:?}"),
}
}
#[test]
fn accepted_mints_in_the_core_then_untrack_removes() {
let relay = web_relay();
let accepted = step_command(&relay, &relay.init(), &RelayCommand::Accepted {
token: 42,
peer: peer_a(),
service: "web".to_string(),
});
let key = SessionKey::new(peer_a(), super::TCP, SessionId(0), Initiator::Local);
match accepted.effects.as_slice() {
[RelayEffect::OpenAccepted {
token,
key: k,
service,
}] => {
assert_eq!(*token, 42);
assert_eq!(*k, key);
assert_eq!(service, "web");
}
other => panic!("expected one OpenAccepted, got {other:?}"),
}
assert!(accepted.state.sessions.contains(&key));
let untracked = step_command(&relay, &accepted.state, &RelayCommand::Untrack {
peer: peer_a(),
session: SessionId(0),
initiator: Initiator::Local,
});
assert!(untracked.effects.is_empty());
assert!(!untracked.state.sessions.contains(&key));
}
#[test]
fn a_peer_cannot_address_another_peers_session() {
let relay = web_relay();
let a_open = step_frame(&relay, &relay.init(), peer_a(), &open(0, "web"));
let key_a = rkey(peer_a(), 0);
assert!(a_open.state.sessions.contains(&key_a));
let b_data = step_frame(&relay, &a_open.state, peer_b(), &data(0, b"x"));
assert!(
b_data.effects.is_empty(),
"B's Data for a session it did not open is dropped"
);
let b_close = step_frame(&relay, &a_open.state, peer_b(), &close(0));
assert!(
b_close.effects.is_empty(),
"B's Close for a session it did not open is dropped"
);
assert!(b_close.state.sessions.contains(&key_a));
}
#[test]
fn local_and_remote_sessions_with_the_same_id_do_not_collide() {
let relay = web_relay();
let opened = step_frame(&relay, &relay.init(), peer_a(), &open(0, "web"));
let accepted = step_command(&relay, &opened.state, &RelayCommand::Accepted {
token: 1,
peer: peer_a(),
service: "web".to_string(),
});
let remote = SessionKey::new(peer_a(), super::TCP, SessionId(0), Initiator::Remote);
let local = SessionKey::new(peer_a(), super::TCP, SessionId(0), Initiator::Local);
assert_ne!(remote, local);
assert!(accepted.state.sessions.contains(&remote));
assert!(accepted.state.sessions.contains(&local));
assert_eq!(accepted.state.sessions.len(), 2);
}
#[test]
fn open_then_close_then_data_does_not_resurrect_the_session() {
let relay = web_relay();
let key = rkey(peer_a(), 3);
let opened = step_frame(&relay, &relay.init(), peer_a(), &open(3, "web"));
assert!(opened.state.sessions.contains(&key));
let closed = step_frame(&relay, &opened.state, peer_a(), &close(3));
assert!(!closed.state.sessions.contains(&key));
let late = step_frame(&relay, &closed.state, peer_a(), &data(3, b"late"));
assert!(late.effects.is_empty());
assert!(late.state.sessions.is_empty());
}
#[test]
fn close_after_close_is_idempotent() {
let relay = web_relay();
let opened = step_frame(&relay, &relay.init(), peer_a(), &open(5, "web"));
let c1 = step_frame(&relay, &opened.state, peer_a(), &close(5));
assert!(matches!(c1.effects.as_slice(), [RelayEffect::Close { .. }]));
let c2 = step_frame(&relay, &c1.state, peer_a(), &close(5));
assert!(c2.effects.is_empty());
assert!(c2.state.sessions.is_empty());
}
#[test]
fn malformed_payload_is_rejected_at_the_boundary() {
let relay = web_relay();
let bad = [0xFFu8, 0xFF, 0xFF, 0xFF, 0xFF];
let result = relay.decode(Wire {
from: peer_a(),
me: this_node(),
payload: &bad,
});
assert!(result.is_err(), "a malformed frame must be rejected");
}
#[test]
fn lifecycle_property_state_never_diverges_from_model() {
let relay = web_relay();
let peers = [peer_a(), peer_b(), Did::from(4u32)];
let mut state = relay.init();
let mut model: HashSet<SessionKey> = HashSet::new();
let mut rng: u64 = 0x2545_F491_4F6C_DD1D;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
for _ in 0..4000 {
let r = next();
let peer = peers[(r % 3) as usize];
let session = (r >> 2) & 0x7; let key = rkey(peer, session);
let transition = match (r >> 8) % 4 {
0 => {
let t = step_frame(&relay, &state, peer, &open(session, "web"));
if model.contains(&key) {
assert!(t.effects.is_empty(), "duplicate Open must emit nothing");
} else {
assert!(matches!(t.effects.as_slice(), [
RelayEffect::Connect { .. }
]));
model.insert(key.clone());
}
t
}
1 => {
let t = step_frame(&relay, &state, peer, &open(session, "nope"));
if model.contains(&key) {
assert!(t.effects.is_empty());
} else {
assert!(matches!(t.effects.as_slice(), [
RelayEffect::SendClose { .. }
]));
}
t
}
2 => {
let t = step_frame(&relay, &state, peer, &data(session, b"x"));
if model.contains(&key) {
match t.effects.as_slice() {
[RelayEffect::Write { key: k, .. }] => assert_eq!(*k, key),
other => panic!("expected one Write, got {other:?}"),
}
} else {
assert!(
t.effects.is_empty(),
"Data on an unknown session is dropped"
);
}
t
}
_ => {
let t = step_frame(&relay, &state, peer, &close(session));
if model.contains(&key) {
assert!(matches!(t.effects.as_slice(), [RelayEffect::Close { .. }]));
} else {
assert!(
t.effects.is_empty(),
"Close on an unknown session is dropped"
);
}
model.remove(&key);
t
}
};
state = transition.state;
assert_eq!(
state.sessions, model,
"State.sessions diverged from the model"
);
}
}
struct EngineModel {
map: HashMap<SessionKey, u64>,
next_gen: u64,
}
impl EngineModel {
fn new() -> Self {
Self {
map: HashMap::new(),
next_gen: 0,
}
}
fn register(&mut self, key: SessionKey) -> u64 {
let gen = self.next_gen;
self.next_gen += 1;
self.map.insert(key, gen);
gen
}
fn close(&mut self, key: &SessionKey) {
self.map.remove(key);
}
fn close_if_current(&mut self, key: &SessionKey, gen: u64) -> bool {
if self.map.get(key) == Some(&gen) {
self.map.remove(key);
true
} else {
false
}
}
}
fn apply_effects(
eng: &mut EngineModel,
effects: &[RelayEffect<SocketAddr>],
) -> Option<(SessionKey, u64)> {
let mut registered = None;
for effect in effects {
match effect {
RelayEffect::Connect { key, .. } | RelayEffect::OpenAccepted { key, .. } => {
registered = Some((key.clone(), eng.register(key.clone())));
}
RelayEffect::Write { key, .. } | RelayEffect::Shutdown { key } => {
assert!(
eng.map.contains_key(key),
"effect targeted a non-live session"
);
}
RelayEffect::Close { key } => eng.close(key),
RelayEffect::SendClose { .. } => {}
}
}
registered
}
#[test]
fn generation_prevents_a_slow_old_task_deleting_a_reopened_handle() {
let relay = web_relay();
let mut eng = EngineModel::new();
let opened = step_frame(&relay, &relay.init(), peer_a(), &open(7, "web"));
let (key, gen_old) = apply_effects(&mut eng, &opened.effects).expect("registered");
let closed = step_frame(&relay, &opened.state, peer_a(), &close(7));
apply_effects(&mut eng, &closed.effects);
assert!(!eng.map.contains_key(&key));
let reopened = step_frame(&relay, &closed.state, peer_a(), &open(7, "web"));
let (_, gen_new) = apply_effects(&mut eng, &reopened.effects).expect("registered");
assert_ne!(gen_old, gen_new);
let removed = eng.close_if_current(&key, gen_old);
assert!(
!removed,
"stale task must not remove — and so must send no peer Close"
);
assert_eq!(
eng.map.get(&key),
Some(&gen_new),
"old task must not delete the reopened handle"
);
}
#[test]
fn engine_model_stays_consistent_with_step_under_interleaving() {
let relay = web_relay();
let peers = [peer_a(), peer_b()];
let mut state = relay.init();
let mut eng = EngineModel::new();
let mut tasks: Vec<(SessionKey, u64)> = Vec::new();
let mut rng: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = move || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
for _ in 0..3000 {
let r = next();
let peer = peers[(r % 2) as usize];
let session = (r >> 2) & 0x3; match (r >> 8) % 4 {
0 => {
let t = step_frame(&relay, &state, peer, &open(session, "web"));
if let Some(task) = apply_effects(&mut eng, &t.effects) {
tasks.push(task);
}
state = t.state;
}
1 => {
let t = step_frame(&relay, &state, peer, &data(session, b"x"));
apply_effects(&mut eng, &t.effects);
state = t.state;
}
2 => {
let t = step_frame(&relay, &state, peer, &close(session));
apply_effects(&mut eng, &t.effects);
state = t.state;
}
_ => {
if !tasks.is_empty() {
let idx = (r >> 16) as usize % tasks.len();
let (tkey, tgen) = tasks.swap_remove(idx);
let removed = eng.close_if_current(&tkey, tgen);
if removed {
let untrack = RelayCommand::Untrack {
peer: tkey.peer,
session: tkey.session,
initiator: tkey.initiator,
};
state = step_command(&relay, &state, &untrack).state;
}
}
}
}
let live: HashSet<SessionKey> = eng.map.keys().cloned().collect();
assert_eq!(
state.sessions, live,
"pure state diverged from the engine model"
);
}
}
}