hopr_transport_probe/
content.rs1use hopr_primitive_types::prelude::GeneralError;
2use hopr_protocol_app::prelude::{ApplicationData, ReservedTag, Tag};
3
4use crate::errors::ProbeError;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumDiscriminants)]
8#[strum_discriminants(vis(pub(crate)))]
9#[strum_discriminants(derive(strum::FromRepr, strum::EnumCount), repr(u8))]
10pub enum NeighborProbe {
11 Ping([u8; Self::NONCE_SIZE]),
13 Pong([u8; Self::NONCE_SIZE]),
15}
16
17impl NeighborProbe {
18 pub const NONCE_SIZE: usize = 32;
19 pub const SIZE: usize = 1 + Self::NONCE_SIZE;
20
21 pub fn random_nonce() -> Self {
23 Self::Ping(hopr_crypto_random::random_bytes::<{ Self::NONCE_SIZE }>())
24 }
25
26 pub fn is_complement_to(&self, other: Self) -> bool {
27 match (self, &other) {
28 (Self::Ping(nonce1), Self::Pong(nonce2)) => nonce1 == nonce2,
29 (Self::Pong(nonce1), Self::Ping(nonce2)) => nonce1 == nonce2,
30 _ => false,
31 }
32 }
33
34 pub fn to_bytes(self) -> Box<[u8]> {
35 let mut out = Vec::with_capacity(1 + Self::NONCE_SIZE);
36 out.push(NeighborProbeDiscriminants::from(&self) as u8);
37 out.extend_from_slice(self.as_ref());
38 out.into_boxed_slice()
39 }
40}
41
42impl<'a> TryFrom<&'a [u8]> for NeighborProbe {
43 type Error = GeneralError;
44
45 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
46 if value.len() == 1 + Self::NONCE_SIZE {
47 match NeighborProbeDiscriminants::from_repr(value[0])
48 .ok_or(GeneralError::ParseError("NeighborProbe.disc".into()))?
49 {
50 NeighborProbeDiscriminants::Ping => {
51 Ok(Self::Ping((&value[1..]).try_into().map_err(|_| {
52 GeneralError::ParseError("NeighborProbe.ping_nonce".into())
53 })?))
54 }
55 NeighborProbeDiscriminants::Pong => {
56 Ok(Self::Pong((&value[1..]).try_into().map_err(|_| {
57 GeneralError::ParseError("NeighborProbe.pong_nonce".into())
58 })?))
59 }
60 }
61 } else {
62 Err(GeneralError::ParseError("NeighborProbe.size".into()))
63 }
64 }
65}
66
67impl std::fmt::Display for NeighborProbe {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 NeighborProbe::Ping(nonce) => write!(f, "Ping({})", hex::encode(nonce)),
71 NeighborProbe::Pong(nonce) => write!(f, "Pong({})", hex::encode(nonce)),
72 }
73 }
74}
75
76impl AsRef<[u8]> for NeighborProbe {
77 fn as_ref(&self) -> &[u8] {
78 match self {
79 NeighborProbe::Ping(nonce) | NeighborProbe::Pong(nonce) => nonce,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub struct PathTelemetry {
87 pub id: [u8; Self::ID_SIZE],
88 pub path: [u8; Self::PATH_SIZE],
89 pub timestamp: u128,
90}
91
92impl PathTelemetry {
93 pub const ID_SIZE: usize = 10;
94 pub const PATH_SIZE: usize = 10;
95 pub const SIZE: usize = Self::ID_SIZE + Self::PATH_SIZE + size_of::<u128>();
96
97 pub fn to_bytes(self) -> Box<[u8]> {
98 let mut out = Vec::with_capacity(Self::SIZE);
99 out.extend_from_slice(&self.id);
100 out.extend_from_slice(&self.path);
101 out.extend_from_slice(&self.timestamp.to_be_bytes());
102 out.into_boxed_slice()
103 }
104}
105
106impl std::fmt::Display for PathTelemetry {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(
109 f,
110 "PathTelemetry(id: {}, path: {}, timestamp: {})",
111 hex::encode(self.id),
112 hex::encode(self.path),
113 self.timestamp
114 )
115 }
116}
117
118impl<'a> TryFrom<&'a [u8]> for PathTelemetry {
119 type Error = GeneralError;
120
121 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
122 if value.len() == Self::SIZE {
123 Ok(Self {
124 id: (&value[0..10])
125 .try_into()
126 .map_err(|_| GeneralError::ParseError("PathTelemetry.id".into()))?,
127 path: (&value[10..20])
128 .try_into()
129 .map_err(|_| GeneralError::ParseError("PathTelemetry.path".into()))?,
130 timestamp: u128::from_be_bytes(
131 (&value[20..36])
132 .try_into()
133 .map_err(|_| GeneralError::ParseError("PathTelemetry.timestamp".into()))?,
134 ),
135 })
136 } else {
137 Err(GeneralError::ParseError("PathTelemetry".into()))
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumDiscriminants, strum::Display)]
143#[strum_discriminants(vis(pub(crate)))]
144#[strum_discriminants(derive(strum::FromRepr, strum::EnumCount), repr(u8))]
145pub enum Message {
146 Telemetry(PathTelemetry),
147 Probe(NeighborProbe),
148}
149
150impl Message {
151 pub const VERSION: u8 = 1;
152
153 pub fn to_bytes(self) -> Box<[u8]> {
154 let mut out = Vec::<u8>::with_capacity(1 + NeighborProbe::SIZE.max(PathTelemetry::SIZE));
155 out.push(Self::VERSION);
156 out.push(MessageDiscriminants::from(&self) as u8);
157
158 match self {
159 Message::Telemetry(telemetry) => out.extend(telemetry.to_bytes()),
160 Message::Probe(probe) => out.extend(probe.to_bytes()),
161 }
162
163 out.into_boxed_slice()
164 }
165}
166
167impl<'a> TryFrom<&'a [u8]> for Message {
168 type Error = GeneralError;
169
170 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
171 if value.is_empty() {
172 return Err(GeneralError::ParseError("Message.size".into()));
173 }
174
175 if value[0] != Self::VERSION {
176 return Err(GeneralError::ParseError("Message.version".into()));
177 }
178
179 match MessageDiscriminants::from_repr(value[1]).ok_or(GeneralError::ParseError("Message.disc".into()))? {
180 MessageDiscriminants::Telemetry => {
181 if value.len() == 2 + PathTelemetry::SIZE {
182 Ok(Self::Telemetry(
183 (&value[2..])
184 .try_into()
185 .map_err(|_| GeneralError::ParseError("Message.telemetry".into()))?,
186 ))
187 } else {
188 Err(GeneralError::ParseError("Message.telemetry.size".into()))?
189 }
190 }
191 MessageDiscriminants::Probe => {
192 if value.len() == 2 + NeighborProbe::SIZE {
193 Ok(Self::Probe(
194 (&value[2..])
195 .try_into()
196 .map_err(|_| GeneralError::ParseError("Message.probe".into()))?,
197 ))
198 } else {
199 Err(GeneralError::ParseError("Message.probe.size".into()))?
200 }
201 }
202 }
203 }
204}
205
206impl TryFrom<Message> for ApplicationData {
207 type Error = ProbeError;
208
209 fn try_from(message: Message) -> Result<Self, Self::Error> {
210 Ok(ApplicationData::new(ReservedTag::Ping, message.to_bytes().into_vec())?)
211 }
212}
213
214impl TryFrom<ApplicationData> for Message {
215 type Error = anyhow::Error;
216
217 fn try_from(value: ApplicationData) -> Result<Self, Self::Error> {
218 let reserved_probe_tag: Tag = ReservedTag::Ping.into();
219
220 if value.application_tag == reserved_probe_tag {
221 let message: Message = value.plain_text.as_ref().try_into()?;
222 Ok(message)
223 } else {
224 Err(anyhow::anyhow!("Non probing application tag found"))
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use hopr_platform::time::native::current_time;
232 use hopr_primitive_types::traits::AsUnixTimestamp;
233 use more_asserts::assert_lt;
234
235 use super::*;
236
237 #[test]
238 fn probe_message_should_serialize_and_deserialize() -> anyhow::Result<()> {
239 let m1 = Message::Probe(NeighborProbe::random_nonce());
240 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
241
242 assert_eq!(m1, m2);
243
244 let m1 = Message::Telemetry(PathTelemetry {
245 id: hopr_crypto_random::random_bytes(),
246 path: hopr_crypto_random::random_bytes(),
247 timestamp: 1234567890,
248 });
249 let m2 = Message::try_from(m1.to_bytes().as_ref())?;
250
251 assert_eq!(m1, m2);
252 Ok(())
253 }
254
255 #[test]
256 fn random_generation_of_a_neighbor_probe_produces_a_ping() {
257 let ping = NeighborProbe::random_nonce();
258 assert!(matches!(ping, NeighborProbe::Ping(_)));
259 }
260
261 #[test]
262 fn check_for_complement_works_for_ping_and_pong_with_the_same_nonce() -> anyhow::Result<()> {
263 let ping = NeighborProbe::random_nonce();
264 let pong = { NeighborProbe::Pong(ping.as_ref().try_into()?) };
265
266 assert!(ping.is_complement_to(pong));
267 Ok(())
268 }
269
270 #[test]
271 fn check_for_complement_fails_for_ping_and_pong_with_different_nonce() -> anyhow::Result<()> {
272 let ping = NeighborProbe::random_nonce();
273 let pong = {
274 let mut other: [u8; NeighborProbe::NONCE_SIZE] = ping.as_ref().try_into()?;
275 other[0] = other[0].wrapping_add(1); NeighborProbe::Pong(other)
277 };
278
279 assert!(!ping.is_complement_to(pong));
280
281 Ok(())
282 }
283
284 #[test]
285 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_direct_probing() -> anyhow::Result<()> {
286 let ping = NeighborProbe::random_nonce();
287 let as_data: ApplicationData = Message::Probe(ping).try_into()?;
288
289 assert_lt!(
290 as_data.plain_text.len(),
291 ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
292 );
293
294 Ok(())
295 }
296
297 #[test]
298 fn check_that_at_least_one_surb_can_fit_into_the_payload_for_path_telemetry() -> anyhow::Result<()> {
299 let telemetry = PathTelemetry {
300 id: [1; 10],
301 path: [1; 10],
302 timestamp: current_time().as_unix_timestamp().as_millis(),
303 };
304 let as_data: ApplicationData = Message::Telemetry(telemetry).try_into()?;
305
306 assert_lt!(
307 as_data.plain_text.len(),
308 ApplicationData::PAYLOAD_SIZE - hopr_crypto_packet::HoprSurb::SIZE
309 );
310
311 Ok(())
312 }
313}