ndn_discovery/hello/
probe.rs

1//! SWIM direct and indirect liveness probe packets.
2//!
3//! SWIM probes use standard NDN Interest/Data so they traverse the normal
4//! forwarding pipeline.  The PIT handles rendezvous for indirect probes — no
5//! special routing is needed.
6//!
7//! ## Packet formats
8//!
9//! **Direct probe** (node A → node B):
10//! ```text
11//! Interest: /ndn/local/nd/probe/direct/<B-name>/<nonce-u32>
12//! ```
13//!
14//! **Indirect probe** (node A asks node C to probe node B):
15//! ```text
16//! Interest: /ndn/local/nd/probe/via/<C-name>/<B-name>/<nonce-u32>
17//! ```
18//!
19//! **Probe ACK** (node B or C → node A via PIT reverse path):
20//! ```text
21//! Data: same name as the Interest
22//!       Content: empty (the ACK is the Data itself)
23//! ```
24//!
25//! ## Usage pattern
26//!
27//! 1. A detects B is `STALE`; sends `build_direct_probe(b, nonce)` on B's face.
28//! 2. If no ACK within `probe_timeout`, A sends
29//!    `build_indirect_probe(c, b, nonce)` for K randomly chosen intermediaries.
30//! 3. Only if all K indirect probes time out does A declare B `ABSENT`.
31//!
32//! The SWIM fanout K is set by [`DiscoveryConfig::swim_indirect_fanout`].
33//! K = 0 disables indirect probing entirely (use [`BackoffScheduler`] instead).
34//!
35//! [`DiscoveryConfig::swim_indirect_fanout`]: crate::config::DiscoveryConfig::swim_indirect_fanout
36//! [`BackoffScheduler`]: crate::strategy::BackoffScheduler
37
38use bytes::Bytes;
39use ndn_packet::{Name, tlv_type};
40use ndn_tlv::TlvWriter;
41
42use crate::scope::{probe_direct, probe_via};
43use crate::wire::{parse_raw_data, parse_raw_interest, write_name_tlv, write_nni};
44
45// ─── Packet builders ──────────────────────────────────────────────────────────
46
47/// Build a direct probe Interest.
48///
49/// Name: `/ndn/local/nd/probe/direct/<target>/<nonce>`
50pub fn build_direct_probe(target: &Name, nonce: u32) -> Bytes {
51    let mut w = TlvWriter::new();
52    w.write_nested(tlv_type::INTEREST, |w: &mut TlvWriter| {
53        w.write_nested(tlv_type::NAME, |w: &mut TlvWriter| {
54            for comp in probe_direct().components() {
55                w.write_tlv(comp.typ, &comp.value);
56            }
57            for comp in target.components() {
58                w.write_tlv(comp.typ, &comp.value);
59            }
60            w.write_tlv(tlv_type::NAME_COMPONENT, &nonce.to_be_bytes());
61        });
62        w.write_tlv(tlv_type::NONCE, &nonce.to_be_bytes());
63        write_nni(w, tlv_type::INTEREST_LIFETIME, 2000);
64    });
65    w.finish()
66}
67
68/// Build an indirect probe Interest.
69///
70/// Name: `/ndn/local/nd/probe/via/<intermediary>/<target>/<nonce>`
71///
72/// The intermediary receives this Interest, looks up the target in its FIB, and
73/// forwards a direct probe on the caller's behalf.
74pub fn build_indirect_probe(intermediary: &Name, target: &Name, nonce: u32) -> Bytes {
75    let mut w = TlvWriter::new();
76    w.write_nested(tlv_type::INTEREST, |w: &mut TlvWriter| {
77        w.write_nested(tlv_type::NAME, |w: &mut TlvWriter| {
78            for comp in probe_via().components() {
79                w.write_tlv(comp.typ, &comp.value);
80            }
81            for comp in intermediary.components() {
82                w.write_tlv(comp.typ, &comp.value);
83            }
84            for comp in target.components() {
85                w.write_tlv(comp.typ, &comp.value);
86            }
87            w.write_tlv(tlv_type::NAME_COMPONENT, &nonce.to_be_bytes());
88        });
89        w.write_tlv(tlv_type::NONCE, &nonce.to_be_bytes());
90        write_nni(w, tlv_type::INTEREST_LIFETIME, 4000);
91    });
92    w.finish()
93}
94
95/// Build a probe ACK Data packet.
96///
97/// The name is echoed verbatim from the Interest; content is empty.
98pub fn build_probe_ack(interest_name: &Name) -> Bytes {
99    let mut w = TlvWriter::new();
100    w.write_nested(tlv_type::DATA, |w: &mut TlvWriter| {
101        write_name_tlv(w, interest_name);
102        w.write_nested(tlv_type::META_INFO, |w: &mut TlvWriter| {
103            write_nni(w, tlv_type::FRESHNESS_PERIOD, 0);
104        });
105        // Empty content — the ACK is the Data itself.
106        w.write_tlv(tlv_type::CONTENT, &[]);
107        w.write_nested(tlv_type::SIGNATURE_INFO, |w: &mut TlvWriter| {
108            w.write_tlv(tlv_type::SIGNATURE_TYPE, &[0u8]);
109        });
110        w.write_tlv(tlv_type::SIGNATURE_VALUE, &[0u8; 32]);
111    });
112    w.finish()
113}
114
115// ─── Packet parsers ───────────────────────────────────────────────────────────
116
117/// Parse a direct probe Interest name.
118///
119/// Returns `(target_name, nonce)` if the name matches
120/// `/ndn/local/nd/probe/direct/<target...>/<nonce>`, `None` otherwise.
121pub fn parse_direct_probe(raw: &Bytes) -> Option<DirectProbe> {
122    let parsed = parse_raw_interest(raw)?;
123    let name = &parsed.name;
124    let prefix = probe_direct();
125
126    if !name.has_prefix(prefix) {
127        return None;
128    }
129
130    let comps = name.components();
131    let prefix_len = prefix.components().len();
132
133    // At minimum: prefix + 1 target component + nonce = prefix_len + 2
134    if comps.len() < prefix_len + 2 {
135        return None;
136    }
137
138    let nonce_comp = &comps[comps.len() - 1];
139    if nonce_comp.value.len() != 4 {
140        return None;
141    }
142    let nonce = u32::from_be_bytes(nonce_comp.value[..4].try_into().ok()?);
143
144    // Target name = all components between prefix and nonce.
145    let target_comps = &comps[prefix_len..comps.len() - 1];
146    let target = Name::from_components(target_comps.iter().cloned());
147
148    Some(DirectProbe { target, nonce })
149}
150
151/// Parse an indirect probe Interest name.
152///
153/// Returns `(intermediary_name, target_name, nonce)` if the name matches
154/// `/ndn/local/nd/probe/via/<intermediary...>/<target...>/<nonce>`.
155///
156/// **Encoding convention**: the intermediary name length is encoded as a
157/// single `NameComponent` carrying a 1-byte count before the intermediary
158/// components.  This is the same convention used by NFD for parameterised
159/// Interest names — the length is explicit so there is no ambiguity between
160/// the end of the intermediary and the start of the target.
161///
162/// Format: `probe/via/<n:u8>/<intermediary×n>/<target...>/<nonce>`
163pub fn parse_indirect_probe(raw: &Bytes) -> Option<IndirectProbe> {
164    let parsed = parse_raw_interest(raw)?;
165    let name = &parsed.name;
166    let prefix = probe_via();
167
168    if !name.has_prefix(prefix) {
169        return None;
170    }
171
172    let comps = name.components();
173    let prefix_len = prefix.components().len();
174
175    // Need at least: prefix + length-byte + 1 intermediary + 1 target + nonce
176    if comps.len() < prefix_len + 4 {
177        return None;
178    }
179
180    // First component after prefix: intermediary component count.
181    let count_comp = &comps[prefix_len];
182    if count_comp.value.len() != 1 {
183        return None;
184    }
185    let intermediary_len = count_comp.value[0] as usize;
186
187    let inter_start = prefix_len + 1;
188    let inter_end = inter_start + intermediary_len;
189    let target_end = comps.len() - 1; // last is nonce
190
191    if inter_end >= target_end {
192        return None; // target would be empty or overlap nonce
193    }
194
195    let nonce_comp = &comps[comps.len() - 1];
196    if nonce_comp.value.len() != 4 {
197        return None;
198    }
199    let nonce = u32::from_be_bytes(nonce_comp.value[..4].try_into().ok()?);
200
201    let intermediary = Name::from_components(comps[inter_start..inter_end].iter().cloned());
202    let target = Name::from_components(comps[inter_end..target_end].iter().cloned());
203
204    Some(IndirectProbe {
205        intermediary,
206        target,
207        nonce,
208    })
209}
210
211/// Build an indirect probe with the length-prefix encoding.
212///
213/// Encodes: `probe/via/<intermediary-len:u8>/<intermediary...>/<target...>/<nonce>`
214pub fn build_indirect_probe_encoded(intermediary: &Name, target: &Name, nonce: u32) -> Bytes {
215    let inter_len = intermediary.components().len();
216    assert!(inter_len <= 255, "intermediary name too long");
217
218    let mut w = TlvWriter::new();
219    w.write_nested(tlv_type::INTEREST, |w: &mut TlvWriter| {
220        w.write_nested(tlv_type::NAME, |w: &mut TlvWriter| {
221            for comp in probe_via().components() {
222                w.write_tlv(comp.typ, &comp.value);
223            }
224            // Length prefix.
225            w.write_tlv(tlv_type::NAME_COMPONENT, &[inter_len as u8]);
226            for comp in intermediary.components() {
227                w.write_tlv(comp.typ, &comp.value);
228            }
229            for comp in target.components() {
230                w.write_tlv(comp.typ, &comp.value);
231            }
232            w.write_tlv(tlv_type::NAME_COMPONENT, &nonce.to_be_bytes());
233        });
234        w.write_tlv(tlv_type::NONCE, &nonce.to_be_bytes());
235        write_nni(w, tlv_type::INTEREST_LIFETIME, 4000);
236    });
237    w.finish()
238}
239
240/// Parsed fields from a direct probe Interest.
241#[derive(Debug, Clone)]
242pub struct DirectProbe {
243    pub target: Name,
244    pub nonce: u32,
245}
246
247/// Parsed fields from an indirect probe Interest.
248#[derive(Debug, Clone)]
249pub struct IndirectProbe {
250    pub intermediary: Name,
251    pub target: Name,
252    pub nonce: u32,
253}
254
255/// Check whether the raw packet is a probe ACK Data (empty-content reply).
256pub fn is_probe_ack(raw: &Bytes) -> bool {
257    let Some(parsed) = parse_raw_data(raw) else {
258        return false;
259    };
260    let name = &parsed.name;
261    name.has_prefix(probe_direct()) || name.has_prefix(probe_via())
262}
263
264// ─── Tests ────────────────────────────────────────────────────────────────────
265
266#[cfg(test)]
267mod tests {
268    use std::str::FromStr;
269
270    use super::*;
271
272    fn n(s: &str) -> Name {
273        Name::from_str(s).unwrap()
274    }
275
276    #[test]
277    fn direct_probe_roundtrip() {
278        let target = n("/ndn/site/nodeB");
279        let nonce = 0xABCD_1234;
280        let pkt = build_direct_probe(&target, nonce);
281
282        let parsed = parse_direct_probe(&pkt).unwrap();
283        assert_eq!(parsed.target, target);
284        assert_eq!(parsed.nonce, nonce);
285    }
286
287    #[test]
288    fn indirect_probe_roundtrip() {
289        let intermediary = n("/ndn/site/nodeC");
290        let target = n("/ndn/site/nodeB");
291        let nonce = 0xDEAD_BEEF;
292        let pkt = build_indirect_probe_encoded(&intermediary, &target, nonce);
293
294        let parsed = parse_indirect_probe(&pkt).unwrap();
295        assert_eq!(parsed.intermediary, intermediary);
296        assert_eq!(parsed.target, target);
297        assert_eq!(parsed.nonce, nonce);
298    }
299
300    #[test]
301    fn probe_ack_is_detected() {
302        let probe_name = n("/ndn/local/nd/probe/direct/ndn/site/nodeB/00000001");
303        let ack = build_probe_ack(&probe_name);
304        assert!(is_probe_ack(&ack));
305    }
306
307    #[test]
308    fn direct_probe_rejects_wrong_prefix() {
309        let other = build_indirect_probe_encoded(&n("/ndn/site/c"), &n("/ndn/site/b"), 1);
310        // parse_direct_probe should reject a via-prefix packet
311        assert!(parse_direct_probe(&other).is_none());
312    }
313}