Skip to main content

rns_core/transport/
path_requests.rs

1use super::*;
2
3pub fn lowest_interface_bitrate(interfaces: &BTreeMap<InterfaceId, InterfaceInfo>) -> Option<u64> {
4    // Having no usable bitrate is a normal state, not a diagnostic condition.
5    // Callers fall back to the fixed path-request timeout without logging.
6    interfaces
7        .values()
8        .filter_map(|interface| interface.bitrate)
9        .filter(|bitrate| *bitrate > 0)
10        .min()
11}
12
13pub fn medium_path_timeout(interfaces: &BTreeMap<InterfaceId, InterfaceInfo>) -> f64 {
14    let Some(lowest_bitrate) = lowest_interface_bitrate(interfaces) else {
15        return 0.0;
16    };
17    let effective_bitrate = lowest_bitrate.max(constants::MINIMUM_BITRATE);
18    2.0 * (constants::MTU as f64 * 8.0 / effective_bitrate as f64)
19        + constants::LINK_ESTABLISHMENT_TIMEOUT_PER_HOP
20}
21
22pub(super) fn discovery_path_request_timeout(
23    interfaces: &BTreeMap<InterfaceId, InterfaceInfo>,
24) -> f64 {
25    constants::PATH_REQUEST_TIMEOUT.max(medium_path_timeout(interfaces))
26}
27
28impl TransportEngine {
29    /// Re-check live path-request egress state immediately before dispatch.
30    #[doc(hidden)]
31    pub fn should_egress_limit_path_request(
32        &mut self,
33        interface_id: InterfaceId,
34        pr_freq: f64,
35        sample_count: usize,
36    ) -> bool {
37        let Some(config) = self
38            .interfaces
39            .get(&interface_id)
40            .map(|info| info.ingress_control)
41        else {
42            return false;
43        };
44        self.ingress_control
45            .should_egress_limit_pr(interface_id, &config, pr_freq, sample_count)
46    }
47
48    pub fn handle_path_request(
49        &mut self,
50        data: &[u8],
51        interface_id: InterfaceId,
52        now: f64,
53    ) -> Vec<TransportAction> {
54        self.handle_path_request_with_ingress_limit(data, interface_id, now, false)
55    }
56
57    /// Handle a path request while preserving an earlier ingress-limiter
58    /// classification made by an external prioritized queue.
59    #[doc(hidden)]
60    pub fn handle_path_request_with_ingress_limit(
61        &mut self,
62        data: &[u8],
63        interface_id: InterfaceId,
64        now: f64,
65        ingress_limited: bool,
66    ) -> Vec<TransportAction> {
67        let Some(request) = self.accept_path_request(data, interface_id, now) else {
68            return Vec::new();
69        };
70        self.handle_accepted_path_request_with_ingress_limit(request, ingress_limited)
71    }
72
73    /// Validate and deduplicate a path request before recording ingress stats.
74    #[doc(hidden)]
75    pub fn accept_path_request(
76        &mut self,
77        data: &[u8],
78        interface_id: InterfaceId,
79        now: f64,
80    ) -> Option<AcceptedPathRequest> {
81        self.parse_path_request(data, interface_id, now)
82    }
83
84    /// Process a request returned by [`Self::accept_path_request`].
85    #[doc(hidden)]
86    pub fn handle_accepted_path_request_with_ingress_limit(
87        &mut self,
88        ctx: AcceptedPathRequest,
89        ingress_limited: bool,
90    ) -> Vec<TransportAction> {
91        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
92            "Path request for {:02x?} on interface {}",
93            &ctx.destination_hash[..4],
94            ctx.interface_id.0,
95        );
96        if ctx.already_in_flight {
97            self.batch_inflight_path_request(&ctx, ingress_limited);
98            return Vec::new();
99        }
100        let from_local_client = self.serves_local_client(ctx.interface_id);
101        // Transport.py:3439-3448: remember who asked for a destination behind
102        // a local client, so the client's path response can be relayed back.
103        if self
104            .next_hop_interface(&ctx.destination_hash)
105            .is_some_and(|iface| self.serves_local_client(iface))
106        {
107            self.pending_local_path_requests
108                .insert(ctx.destination_hash, (ctx.interface_id, ctx.now));
109        }
110        // Transport.py:3450-3457: a destination local to this system answers
111        // with a path-response announce on the requesting interface, whether
112        // or not transport is enabled. The caller holds the keys to build it.
113        if self.local_destinations.contains_key(&ctx.destination_hash) {
114            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
115                "Answering path request for {:02x?}: destination is local",
116                &ctx.destination_hash[..4],
117            );
118            return alloc::vec![TransportAction::AnnounceLocalPathResponse {
119                destination_hash: ctx.destination_hash,
120                tag: ctx.tag[..ctx.tag_len].to_vec(),
121                interface: ctx.interface_id,
122            }];
123        }
124        // Transport.py:3459: a shared instance answers its local clients from
125        // the path table even without transport.
126        if (self.config.transport_enabled || from_local_client)
127            && self.handle_known_path_request(&ctx, from_local_client)
128        {
129            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
130                "Answering path request for {:02x?}: path is known",
131                &ctx.destination_hash[..4],
132            );
133            return Vec::new();
134        }
135        if from_local_client {
136            // Transport.py:3523-3530: forward on all other interfaces.
137            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
138                "Forwarding path request from local client for {:02x?} to all other interfaces",
139                &ctx.destination_hash[..4],
140            );
141            return self.forward_path_request(&ctx, |info| info.id != ctx.interface_id, false);
142        }
143        if self.config.transport_enabled && self.discovery_search_mode(ctx.interface_id).is_some() {
144            return self.handle_discovery_path_request(&ctx, ingress_limited);
145        }
146        if self.serves_local_clients() {
147            // Transport.py:3585-3590: forward on all local client interfaces;
148            // a client's path response is relayed back to the requester.
149            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
150                "Forwarding path request for {:02x?} to local clients",
151                &ctx.destination_hash[..4],
152            );
153            return self.forward_path_request(&ctx, |info| info.is_local_client, true);
154        }
155        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
156            "Ignoring path request for {:02x?}: no path known",
157            &ctx.destination_hash[..4],
158        );
159        Vec::new()
160    }
161
162    /// Send a new path request for `ctx`'s destination on the interfaces
163    /// selected by `select`, as `Transport.request_path` does without a tag.
164    fn forward_path_request(
165        &mut self,
166        ctx: &AcceptedPathRequest,
167        select: impl Fn(&InterfaceInfo) -> bool,
168        relay_to_local_clients: bool,
169    ) -> Vec<TransportAction> {
170        // Since Reticulum 1.6.0 (upstream d3ec00cd, 156c7323) a relayed path
171        // request keeps the original tag: the stamp covers the tag, and the
172        // tag was recorded on acceptance so echoes are dropped. During the
173        // transition, a stamping node marks an unstamped request relayed to
174        // local clients with the 0xFF sentinel flags byte (upstream 7c1b6486).
175        let tag = &ctx.tag[..ctx.tag_len];
176        if !relay_to_local_clients && self.stamp_prs && ctx.ext_payload.is_none() {
177            // Transport.py:3826-3828 with STAMP_PRS: `request_path` finds no
178            // stamp for the client's tag and queues one stamper job per
179            // egress interface. The stamp is ground off the engine thread and
180            // the request goes out once it is ready.
181            return self
182                .interfaces
183                .values()
184                .filter(|info| info.out_capable && select(info))
185                .map(|info| TransportAction::StampPathRequest {
186                    destination_hash: ctx.destination_hash,
187                    tag: tag.to_vec(),
188                    interface: info.id,
189                })
190                .collect();
191        }
192        let ext_payload = ctx
193            .ext_payload
194            .or_else(|| (relay_to_local_clients && self.stamp_prs).then(unstamped_relay_payload));
195
196        // Path requests always carry the transport identity since 1.6.0.
197        let Some((raw, _)) = build_path_request_packet(
198            &ctx.destination_hash,
199            self.config.identity_hash.as_ref(),
200            tag,
201            ext_payload.as_ref(),
202        ) else {
203            return Vec::new();
204        };
205        self.interfaces
206            .values()
207            .filter(|info| info.out_capable && select(info))
208            .map(|info| TransportAction::SendOnInterface {
209                interface: info.id,
210                raw: raw.clone().into(),
211            })
212            .collect()
213    }
214
215    fn parse_path_request(
216        &mut self,
217        data: &[u8],
218        interface_id: InterfaceId,
219        now: f64,
220    ) -> Option<AcceptedPathRequest> {
221        if data.len() < 16 {
222            return None;
223        }
224
225        let mut destination_hash = [0u8; 16];
226        destination_hash.copy_from_slice(&data[..16]);
227
228        if data.len() <= 16 {
229            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
230                "Ignoring tagless path request for {:02x?}",
231                &destination_hash[..4],
232            );
233            return None;
234        }
235
236        // Since upstream cda4d72b only the legacy (destination hash and tag),
237        // unstamped (with transport id) and stamped (with a 16-byte extension
238        // payload) lengths are processed; any other tagged request, such as
239        // one with an oversized tag, is dropped (the driver counts the
240        // protocol violation).
241        if !constants::is_valid_path_request_length(data.len()) {
242            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
243                "Invalid path request size {} for {:02x?}",
244                data.len(),
245                &destination_hash[..4],
246            );
247            return None;
248        }
249        let ext_payload = if data.len() == constants::PATH_REQUEST_STAMPED_LENGTH {
250            let mut ext = [0u8; constants::PR_EXT_PAYLOAD_LENGTH];
251            ext.copy_from_slice(&data[constants::PATH_REQUEST_LENGTH..]);
252            Some(ext)
253        } else {
254            None
255        };
256        let tag_bytes = if data.len() == constants::PATH_REQUEST_LEGACY_LENGTH {
257            &data[16..32]
258        } else {
259            &data[32..48]
260        };
261
262        let tag_len = tag_bytes.len();
263
264        let mut carried_stamp_value = None;
265        let mut stamped = false;
266        if constants::STAMPS_ENABLED {
267            // Diagnostics follow upstream 2c142973: every request carrying a
268            // stamp extension logs its value on the pathing target, as stamped
269            // or, for the 0xFF sentinel of a stamp-aware relay, as
270            // lenient-relayed, followed by why it fails when it does. Plain
271            // unstamped requests stay at trace until enforcement makes them a
272            // drop.
273            let valid = match ext_payload {
274                None => {
275                    log::trace!("Unstamped path request for {:02x?}", &destination_hash[..4]);
276                    false
277                }
278                Some(ext) if ext[0] != constants::EXT_PAYLOAD_MARKER => {
279                    log::trace!(target: crate::logging::PATHING_LOG_TARGET,
280                        "Unrecognized stamp payload in path request for {:02x?}",
281                        &destination_hash[..4]
282                    );
283                    false
284                }
285                Some(ext) => {
286                    let mut material = Vec::with_capacity(16 + tag_len);
287                    material.extend_from_slice(&destination_hash);
288                    material.extend_from_slice(&tag_bytes[..tag_len]);
289                    let workblock = crate::stamp::stamp_workblock(&material, 0);
290                    let stamp = &ext[constants::EXT_HEADER_LENGTH..];
291                    let value = crate::stamp::stamp_value(&workblock, stamp);
292                    // The receiving interface's `pr_cost` (9b464382).
293                    let required_value = self
294                        .interfaces
295                        .get(&interface_id)
296                        .map(|info| info.stamp_costs.pr_cost(&self.stamp_requirements))
297                        .unwrap_or(self.stamp_requirements.path_request_cost);
298                    let valid = crate::stamp::stamp_valid(stamp, required_value, &workblock);
299                    carried_stamp_value = Some(value);
300                    stamped = valid;
301                    let relayed = ext[1] == constants::PR_FLAG_UNSTAMPED_RELAY;
302                    if relayed {
303                        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
304                            "Lenient-relayed path request for {:02x?} with value {value}",
305                            &destination_hash[..4]
306                        );
307                    } else {
308                        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
309                            "Stamped path request for {:02x?} with value {value}",
310                            &destination_hash[..4]
311                        );
312                    }
313                    if !valid && relayed {
314                        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
315                            "Unstamped relayed path request from stamp-aware node for {:02x?}",
316                            &destination_hash[..4]
317                        );
318                    } else if !valid {
319                        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
320                            "Invalid or insufficient (v:{value}) path request stamp for {:02x?}",
321                            &destination_hash[..4]
322                        );
323                    }
324                    valid
325                }
326            };
327            if self.stamp_requirements.enforced && !valid {
328                return None;
329            }
330        }
331
332        let mut unique_tag = [0u8; 32];
333        unique_tag[..16].copy_from_slice(&destination_hash);
334        unique_tag[16..16 + tag_len].copy_from_slice(&tag_bytes[..tag_len]);
335        if !self.insert_discovery_pr_tag(unique_tag) {
336            return None;
337        }
338
339        let already_in_flight = self.path_requests.contains_key(&destination_hash)
340            && !self.path_table.contains_key(&destination_hash)
341            && !self.local_destinations.contains_key(&destination_hash);
342        self.path_requests.entry(destination_hash).or_insert(now);
343
344        let mut tag = [0u8; 16];
345        tag[..tag_len].copy_from_slice(&tag_bytes[..tag_len]);
346        Some(AcceptedPathRequest {
347            tag,
348            tag_len,
349            interface_id,
350            now,
351            destination_hash,
352            already_in_flight,
353            ext_payload,
354            stamp_value: carried_stamp_value,
355            stamped,
356        })
357    }
358
359    fn batch_inflight_path_request(&mut self, ctx: &AcceptedPathRequest, ingress_limited: bool) {
360        let Some((ingress_control, ip_freq, started)) = self
361            .interfaces
362            .get(&ctx.interface_id)
363            .map(|info| (info.ingress_control, info.ip_freq, info.started))
364        else {
365            return;
366        };
367        if ingress_limited
368            || self.ingress_control.should_ingress_limit_pr(
369                ctx.interface_id,
370                &ingress_control,
371                ip_freq,
372                started,
373                ctx.now,
374            )
375        {
376            return;
377        }
378
379        let timeout = discovery_path_request_timeout(&self.interfaces);
380        let request = self
381            .discovery_path_requests
382            .entry(ctx.destination_hash)
383            .or_insert_with(|| DiscoveryPathRequest {
384                timestamp: ctx.now,
385                requesting_interfaces: Vec::new(),
386                engaged: false,
387            });
388        if !request.requesting_interfaces.contains(&ctx.interface_id) {
389            request.requesting_interfaces.push(ctx.interface_id);
390        }
391        self.discovery_path_request_deadlines
392            .entry(ctx.destination_hash)
393            .or_insert(ctx.now + timeout);
394    }
395
396    /// Record a locally generated path request, refreshing its gate timeout.
397    #[doc(hidden)]
398    pub fn record_outbound_path_request(&mut self, destination_hash: [u8; 16], now: f64) {
399        self.path_requests.insert(destination_hash, now);
400    }
401
402    /// Whether this node stamps the path requests it emits and the unstamped
403    /// ones it forwards for local clients (`Transport.STAMP_PRS`).
404    pub fn stamp_prs(&self) -> bool {
405        self.stamp_prs
406    }
407
408    /// Override the compile-time `STAMP_PRS` default.
409    #[doc(hidden)]
410    pub fn set_stamp_prs(&mut self, enabled: bool) {
411        self.stamp_prs = enabled;
412    }
413
414    /// Stamp requirements applied to received announces and path requests.
415    pub fn stamp_requirements(&self) -> StampRequirements {
416        self.stamp_requirements
417    }
418
419    /// Destination type filter applied to received announces.
420    pub fn destination_filter(&self) -> &super::destination_filter::DestinationTypeFilter {
421        &self.destination_filter
422    }
423
424    /// Mutable access to the destination type filter, for `[filter]` and
425    /// the `wants_destination_type` RPC calls.
426    pub fn destination_filter_mut(
427        &mut self,
428    ) -> &mut super::destination_filter::DestinationTypeFilter {
429        &mut self.destination_filter
430    }
431
432    /// Override the compile-time `STAMPS_ENFORCED`, `DESTINATION_COST` and
433    /// `PR_COST` defaults, as the `[stamps]` config section does upstream.
434    pub fn set_stamp_requirements(&mut self, requirements: StampRequirements) {
435        self.stamp_requirements = requirements;
436    }
437
438    /// Build a path request frame for `destination_hash` as this node emits
439    /// it: `destination_hash || transport_id || tag`, plus the optional stamp
440    /// extension payload.
441    ///
442    /// This is the frame `Transport.request_path` sends, with or without the
443    /// `ext_payload` a stamper job produced.
444    pub fn build_path_request(
445        &self,
446        destination_hash: &[u8; 16],
447        tag: &[u8],
448        ext_payload: Option<&[u8; constants::PR_EXT_PAYLOAD_LENGTH]>,
449    ) -> Option<Vec<u8>> {
450        build_path_request_packet(
451            destination_hash,
452            self.config.identity_hash.as_ref(),
453            tag,
454            ext_payload,
455        )
456        .map(|(raw, _)| raw)
457    }
458
459    fn handle_known_path_request(
460        &mut self,
461        ctx: &AcceptedPathRequest,
462        from_local_client: bool,
463    ) -> bool {
464        let Some(path) = self
465            .path_table
466            .get(&ctx.destination_hash)
467            .and_then(|ps| ps.primary())
468            .cloned()
469        else {
470            return false;
471        };
472
473        if let Some(recv_info) = self.interfaces.get(&ctx.interface_id) {
474            if recv_info.mode == constants::MODE_ROAMING
475                && path.receiving_interface == ctx.interface_id
476            {
477                return true;
478            }
479        }
480
481        let Some(raw) = path.announce_raw.as_ref() else {
482            return false;
483        };
484        // Leave the active entry in place until replacement so insertion can
485        // preserve its pending send deadline under repeated path requests.
486        if let Some(existing) = self.announce_table.get(&ctx.destination_hash).cloned() {
487            self.insert_held_announce(ctx.destination_hash, existing, ctx.now);
488        }
489        // Transport.py:3492-3506: answer local clients, and requests for
490        // destinations behind a local client, immediately.
491        let retransmit_timeout =
492            if from_local_client || self.interface_is_local_client(path.receiving_interface) {
493                ctx.now
494            } else if let Some(iface_info) = self.interfaces.get(&ctx.interface_id) {
495                let base = ctx.now + constants::PATH_REQUEST_GRACE;
496                if iface_info.mode == constants::MODE_ROAMING {
497                    base + constants::PATH_REQUEST_RG
498                } else {
499                    base
500                }
501            } else {
502                ctx.now + constants::PATH_REQUEST_GRACE
503            };
504
505        let Ok(parsed) = RawPacket::unpack(raw) else {
506            return false;
507        };
508
509        let entry = AnnounceEntry {
510            timestamp: ctx.now,
511            retransmit_timeout,
512            retries: constants::PATHFINDER_R,
513            received_from: path.next_hop,
514            hops: path.hops,
515            packet_raw: raw.clone(),
516            packet_data: parsed.data().to_vec(),
517            destination_hash: ctx.destination_hash,
518            context_flag: parsed.flags.context_flag,
519            local_rebroadcasts: 0,
520            block_rebroadcasts: true,
521            attached_interface: Some(ctx.interface_id),
522        };
523
524        self.insert_announce_entry(ctx.destination_hash, entry, ctx.now);
525        true
526    }
527
528    /// Whether a path request on `interface_id` searches for unknown paths
529    /// (`should_search_for_unknown` in Transport.py), and in which modes.
530    fn discovery_search_mode(&self, interface_id: InterfaceId) -> Option<Option<&'static [u8]>> {
531        self.interfaces
532            .get(&interface_id)
533            .and_then(|info| search_mode_filter(info.mode, info.recursive_prs))
534    }
535
536    fn handle_discovery_path_request(
537        &mut self,
538        ctx: &AcceptedPathRequest,
539        ingress_limited: bool,
540    ) -> Vec<TransportAction> {
541        let Some((mode, recursive_prs, ingress_control, ip_freq, started)) =
542            self.interfaces.get(&ctx.interface_id).map(|info| {
543                (
544                    info.mode,
545                    info.recursive_prs,
546                    info.ingress_control,
547                    info.ip_freq,
548                    info.started,
549                )
550            })
551        else {
552            return Vec::new();
553        };
554
555        let Some(search_mode_filter) = search_mode_filter(mode, recursive_prs) else {
556            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
557                "Not discovering path to {:02x?}: recursive path discovery is disabled on interface {}",
558                &ctx.destination_hash[..4],
559                ctx.interface_id.0,
560            );
561            return Vec::new();
562        };
563
564        if ingress_limited
565            || self.ingress_control.should_ingress_limit_pr(
566                ctx.interface_id,
567                &ingress_control,
568                ip_freq,
569                started,
570                ctx.now,
571            )
572        {
573            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
574                "Not discovering path to {:02x?}: ingress path-request limiting is active on interface {}",
575                &ctx.destination_hash[..4],
576                ctx.interface_id.0,
577            );
578            return Vec::new();
579        }
580
581        let egress_candidates: Vec<_> = self
582            .interfaces
583            .values()
584            .filter(|info| info.id != ctx.interface_id && info.out_capable)
585            .filter(|info| search_mode_filter.is_none_or(|modes| modes.contains(&info.mode)))
586            .map(|info| {
587                (
588                    info.id,
589                    info.ingress_control,
590                    info.op_freq,
591                    info.op_samples,
592                    info.bitrate,
593                    info.airtime_profile,
594                    info.announce_cap,
595                )
596            })
597            .collect();
598
599        // Upstream 7c1b6486: during the transition a stamping node marks an
600        // unstamped recursive request with the 0xFF sentinel instead of
601        // generating a stamp for a request it did not originate.
602        let ext_payload = ctx
603            .ext_payload
604            .or_else(|| self.stamp_prs.then(unstamped_relay_payload));
605        let Some((path_request_raw, path_request_len)) = build_path_request_packet(
606            &ctx.destination_hash,
607            self.config.identity_hash.as_ref(),
608            &ctx.tag[..ctx.tag_len],
609            ext_payload.as_ref(),
610        ) else {
611            return Vec::new();
612        };
613
614        let mut actions = Vec::new();
615        for (id, ingress_control, op_freq, op_samples, bitrate, airtime_profile, announce_cap) in
616            egress_candidates
617        {
618            if self.ingress_control.should_egress_limit_pr(
619                id,
620                &ingress_control,
621                op_freq,
622                op_samples,
623            ) || self
624                .announce_queues
625                .blocks_recursive_path_request(id, ctx.now)
626            {
627                continue;
628            }
629
630            self.announce_queues.reserve_recursive_path_request(
631                id,
632                path_request_len + constants::HEADER_MINSIZE,
633                ctx.now,
634                bitrate,
635                airtime_profile,
636                announce_cap,
637            );
638            actions.push(TransportAction::SendOnInterface {
639                interface: id,
640                raw: path_request_raw.clone().into(),
641            });
642        }
643
644        if !actions.is_empty() {
645            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
646                "Discovering unknown path to {:02x?} on behalf of interface {} via {} interfaces",
647                &ctx.destination_hash[..4],
648                ctx.interface_id.0,
649                actions.len(),
650            );
651            let request = self
652                .discovery_path_requests
653                .entry(ctx.destination_hash)
654                .or_insert_with(|| DiscoveryPathRequest {
655                    timestamp: ctx.now,
656                    requesting_interfaces: Vec::new(),
657                    engaged: false,
658                });
659            if !request.requesting_interfaces.contains(&ctx.interface_id) {
660                request.requesting_interfaces.push(ctx.interface_id);
661            }
662            request.engaged = true;
663            let timeout = discovery_path_request_timeout(&self.interfaces);
664            self.discovery_path_request_deadlines
665                .insert(ctx.destination_hash, ctx.now + timeout);
666        } else {
667            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
668                "Not discovering path to {:02x?}: no eligible egress interface",
669                &ctx.destination_hash[..4],
670            );
671        }
672
673        actions
674    }
675}
676
677/// Interface modes to search for an unknown path requested on an interface
678/// in `mode`: `None` if it does not search, `Some(None)` for all modes.
679fn search_mode_filter(mode: u8, recursive_prs: bool) -> Option<Option<&'static [u8]>> {
680    if recursive_prs || constants::DISCOVER_PATHS_FOR.contains(&mode) {
681        Some(None)
682    } else if mode == constants::MODE_BOUNDARY {
683        Some(Some(&constants::BOUNDARY_SEARCH_MODES))
684    } else {
685        None
686    }
687}
688
689/// Transitional extension payload marking an unstamped request relayed by a
690/// stamp-aware node to its local clients.
691pub(crate) fn unstamped_relay_payload() -> [u8; constants::PR_EXT_PAYLOAD_LENGTH] {
692    let mut ext = [0u8; constants::PR_EXT_PAYLOAD_LENGTH];
693    ext[0] = constants::EXT_PAYLOAD_MARKER;
694    ext[1] = constants::PR_FLAG_UNSTAMPED_RELAY;
695    ext
696}
697
698/// Build a path request frame, optionally appending a stamp extension.
699///
700/// The extension is opaque here: the caller supplies the verified payload so
701/// a relayed request keeps the exact `marker | flags | stamp` bytes that the
702/// original sender signed the stamp for.
703fn build_path_request_packet(
704    destination_hash: &[u8; 16],
705    transport_identity_hash: Option<&[u8; 16]>,
706    tag: &[u8],
707    ext_payload: Option<&[u8; constants::PR_EXT_PAYLOAD_LENGTH]>,
708) -> Option<(Vec<u8>, usize)> {
709    let mut data = Vec::with_capacity(
710        16 + transport_identity_hash.map_or(0, |_| 16)
711            + tag.len()
712            + ext_payload.map_or(0, |_| constants::PR_EXT_PAYLOAD_LENGTH),
713    );
714    data.extend_from_slice(destination_hash);
715    if let Some(identity_hash) = transport_identity_hash {
716        data.extend_from_slice(identity_hash);
717    }
718    data.extend_from_slice(tag);
719    if let Some(ext) = ext_payload {
720        data.extend_from_slice(ext);
721    }
722
723    let flags = crate::packet::PacketFlags {
724        header_type: constants::HEADER_1,
725        context_flag: constants::FLAG_UNSET,
726        transport_type: constants::TRANSPORT_BROADCAST,
727        destination_type: constants::DESTINATION_PLAIN,
728        packet_type: constants::PACKET_TYPE_DATA,
729    };
730    let path_request_dest =
731        crate::destination::destination_hash("rnstransport", &["path", "request"], None);
732
733    let data_len = data.len();
734    RawPacket::pack(
735        flags,
736        0,
737        &path_request_dest,
738        None,
739        constants::CONTEXT_NONE,
740        &data,
741    )
742    .ok()
743    .map(|packet| (packet.raw, data_len))
744}