rings_core/message/protocols/relay/mod.rs
1#![deny(missing_docs)]
2
3use serde::Deserialize;
4use serde::Serialize;
5
6use crate::dht::Did;
7use crate::error::Error;
8use crate::error::Result;
9
10/// Policy used when sending a report for a request payload.
11///
12/// The default preserves the legacy path-return behavior. Routed returns are
13/// opt-in and send the report as a fresh Chord-routed payload to the declared
14/// destination.
15#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum ReportReturnPolicy {
17 /// Return the report through the reversed relay path.
18 #[default]
19 Path,
20 /// Route the report normally through Chord to this destination.
21 Routed {
22 /// DID that should receive the report.
23 destination: Did,
24 },
25}
26
27impl ReportReturnPolicy {
28 /// Validate that this policy is authorized by the signed request origin.
29 pub fn validate_authorized_by(&self, signer: Did) -> Result<()> {
30 match self {
31 Self::Path => Ok(()),
32 Self::Routed { destination } if *destination == signer => Ok(()),
33 Self::Routed { destination } => Err(Error::InvalidMessage(format!(
34 "routed report return destination {destination} is not signed by that destination"
35 ))),
36 }
37 }
38}
39
40/// MessageRelay guide message passing on rings network by relay.
41///
42/// All messages should be sent with `MessageRelay`.
43/// By calling `relay` method in correct place, `MessageRelay` help to do things:
44/// - Record the whole transport path for inspection.
45/// - Get the sender of a message.
46#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
47pub struct MessageRelay {
48 /// A push only stack. Record routes when handling messages.
49 pub path: Vec<Did>,
50
51 /// The next node to handle the message.
52 /// A message handler will pick transport by this field.
53 pub next_hop: Did,
54
55 /// The destination of the message.
56 /// It may help the handler to find out `next_hop` in some situations.
57 pub destination: Did,
58}
59
60impl MessageRelay {
61 /// Create a new `MessageRelay`.
62 pub fn new(path: Vec<Did>, next_hop: Did, destination: Did) -> Self {
63 Self {
64 path,
65 next_hop,
66 destination,
67 }
68 }
69
70 /// Validate relay, then create a new `MessageRelay` that have `current` did in the end of path.
71 /// The new relay will use `next_hop` as `next_hop` and `self.destination` as `destination`.
72 pub fn forward(&self, current: Did, next_hop: Did) -> Result<Self> {
73 self.validate(current)?;
74
75 if self.next_hop != current {
76 return Err(Error::InvalidNextHop);
77 }
78
79 let mut path = self.path.clone();
80 path.push(current);
81
82 Ok(Self {
83 path,
84 next_hop,
85 destination: self.destination,
86 })
87 }
88
89 /// Validate relay, then create a new `MessageRelay` that used to report the message.
90 /// The new relay will use `self.path[self.path.len() - 1]` as `next_hop` and `self.sender()` as `destination`.
91 /// In the new relay, the path will be cleared and only have `current` did.
92 pub fn path_report(&self, current: Did) -> Result<Self> {
93 self.validate(current)?;
94
95 if self.path.is_empty() {
96 return Err(Error::CannotInferNextHop);
97 }
98
99 Ok(Self {
100 path: vec![current],
101 next_hop: self.path.last().copied().ok_or(Error::CannotInferNextHop)?,
102 destination: self.try_origin_sender()?,
103 })
104 }
105
106 /// Validate relay, then create a fresh Chord-routed report relay.
107 ///
108 /// The caller must infer `next_hop` from the destination before invoking
109 /// this constructor.
110 pub fn routed_report(&self, current: Did, destination: Did, next_hop: Did) -> Result<Self> {
111 self.validate(current)?;
112
113 Ok(Self {
114 path: vec![current],
115 next_hop,
116 destination,
117 })
118 }
119
120 /// Create a report relay with an explicit return policy.
121 pub fn report(
122 &self,
123 current: Did,
124 policy: ReportReturnPolicy,
125 routed_next_hop: Option<Did>,
126 ) -> Result<Self> {
127 match policy {
128 ReportReturnPolicy::Path => self.path_report(current),
129 ReportReturnPolicy::Routed { destination } => self.routed_report(
130 current,
131 destination,
132 routed_next_hop.ok_or(Error::CannotInferNextHop)?,
133 ),
134 }
135 }
136
137 /// Sometime the sender may not know the destination of the message. They just use next_hop as destination.
138 /// The next node can find a new next_hop, and may use this function to set that next_hop as destination again.
139 pub fn reset_destination(&self, destination: Did) -> Self {
140 let mut relay = self.clone();
141 relay.destination = destination;
142 relay
143 }
144
145 /// Check if path and destination is valid.
146 pub fn validate(&self, current: Did) -> Result<()> {
147 if self.next_hop != current {
148 return Err(Error::InvalidNextHop);
149 }
150
151 // Adjacent elements in self.path cannot be equal
152 if self
153 .path
154 .windows(2)
155 .any(|window| matches!(window, [left, right] if left == right))
156 {
157 return Err(Error::InvalidRelayPath);
158 }
159
160 // Prevent infinite loop
161 if has_infinite_loop(&self.path) {
162 tracing::error!("Infinite path detected {:?}", self.path);
163 return Err(Error::InfiniteRelayPath);
164 }
165
166 Ok(())
167 }
168
169 /// Get the origin sender of current message.
170 /// Should be the first element of path.
171 #[deprecated(note = "please use `origin_sender` instead")]
172 pub fn sender(&self) -> Did {
173 self.origin_sender()
174 }
175
176 /// Get the origin sender of current message as a checked relay-path boundary.
177 pub fn try_origin_sender(&self) -> Result<Did> {
178 self.path.first().copied().ok_or(Error::CannotInferNextHop)
179 }
180
181 /// Get the origin sender of current message.
182 ///
183 /// The origin should be the first element of `path`. Empty relay paths keep
184 /// the legacy fallback to `destination`; callers that must distinguish an
185 /// invalid relay boundary from a real origin should use
186 /// [`try_origin_sender`](Self::try_origin_sender).
187 pub fn origin_sender(&self) -> Did {
188 self.path.first().copied().unwrap_or(self.destination)
189 }
190}
191
192// Since rust cannot zip N iterators, when you change this number,
193// you should also change the code of `has_infinite_loop` below.
194const INFINITE_LOOP_TOLERANCE: usize = 3;
195
196fn has_infinite_loop<T>(path: &[T]) -> bool
197where T: PartialEq {
198 // Invariant: a relay loop is witnessed by a non-empty suffix period P such
199 // that the final path segment is P repeated INFINITE_LOOP_TOLERANCE times.
200 for period in 1..=path.len() / INFINITE_LOOP_TOLERANCE {
201 let repeated_len = period * INFINITE_LOOP_TOLERANCE;
202 let start = path.len() - repeated_len;
203 let Some(suffix) = path.get(start..) else {
204 continue;
205 };
206 let mut chunks = suffix.chunks_exact(period);
207 let Some(first) = chunks.next() else {
208 continue;
209 };
210 if chunks.all(|chunk| chunk == first) {
211 return true;
212 }
213 }
214
215 false
216}
217
218#[cfg(test)]
219mod test_relay;