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);
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);
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    ) -> Vec<TransportAction> {
169        // Python uses a fresh random tag. The engine has no RNG here, so
170        // derive one from the request that was answered; it differs from the
171        // incoming tag and is recorded so echoes of it are dropped.
172        let mut seed = Vec::with_capacity(16 + 16 + 8 + 8);
173        seed.extend_from_slice(&ctx.destination_hash);
174        seed.extend_from_slice(&ctx.tag[..ctx.tag_len]);
175        seed.extend_from_slice(&ctx.interface_id.0.to_be_bytes());
176        seed.extend_from_slice(&ctx.now.to_bits().to_be_bytes());
177        let tag = hash::truncated_hash(&seed);
178        let mut unique_tag = [0u8; 32];
179        unique_tag[..16].copy_from_slice(&ctx.destination_hash);
180        unique_tag[16..].copy_from_slice(&tag);
181        self.insert_discovery_pr_tag(unique_tag);
182
183        let transport_identity_hash = if self.config.transport_enabled {
184            self.config.identity_hash.as_ref()
185        } else {
186            None
187        };
188        let Some((raw, _)) =
189            build_path_request_packet(&ctx.destination_hash, transport_identity_hash, &tag)
190        else {
191            return Vec::new();
192        };
193        self.interfaces
194            .values()
195            .filter(|info| info.out_capable && select(info))
196            .map(|info| TransportAction::SendOnInterface {
197                interface: info.id,
198                raw: raw.clone().into(),
199            })
200            .collect()
201    }
202
203    fn parse_path_request(
204        &mut self,
205        data: &[u8],
206        interface_id: InterfaceId,
207        now: f64,
208    ) -> Option<AcceptedPathRequest> {
209        if data.len() < 16 {
210            return None;
211        }
212
213        let mut destination_hash = [0u8; 16];
214        destination_hash.copy_from_slice(&data[..16]);
215
216        let tag_bytes = if data.len() > 32 {
217            Some(&data[32..])
218        } else if data.len() > 16 {
219            Some(&data[16..])
220        } else {
221            None
222        };
223        let Some(tag_bytes) = tag_bytes else {
224            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
225                "Ignoring tagless path request for {:02x?}",
226                &destination_hash[..4],
227            );
228            return None;
229        };
230
231        let tag_len = tag_bytes.len().min(16);
232        let mut unique_tag = [0u8; 32];
233        unique_tag[..16].copy_from_slice(&destination_hash);
234        unique_tag[16..16 + tag_len].copy_from_slice(&tag_bytes[..tag_len]);
235        if !self.insert_discovery_pr_tag(unique_tag) {
236            return None;
237        }
238
239        let already_in_flight = self.path_requests.contains_key(&destination_hash)
240            && !self.path_table.contains_key(&destination_hash)
241            && !self.local_destinations.contains_key(&destination_hash);
242        self.path_requests.entry(destination_hash).or_insert(now);
243
244        let mut tag = [0u8; 16];
245        tag[..tag_len].copy_from_slice(&tag_bytes[..tag_len]);
246        Some(AcceptedPathRequest {
247            tag,
248            tag_len,
249            interface_id,
250            now,
251            destination_hash,
252            already_in_flight,
253        })
254    }
255
256    fn batch_inflight_path_request(&mut self, ctx: &AcceptedPathRequest, ingress_limited: bool) {
257        let Some((ingress_control, ip_freq, started)) = self
258            .interfaces
259            .get(&ctx.interface_id)
260            .map(|info| (info.ingress_control, info.ip_freq, info.started))
261        else {
262            return;
263        };
264        if ingress_limited
265            || self.ingress_control.should_ingress_limit_pr(
266                ctx.interface_id,
267                &ingress_control,
268                ip_freq,
269                started,
270                ctx.now,
271            )
272        {
273            return;
274        }
275
276        let timeout = discovery_path_request_timeout(&self.interfaces);
277        let request = self
278            .discovery_path_requests
279            .entry(ctx.destination_hash)
280            .or_insert_with(|| DiscoveryPathRequest {
281                timestamp: ctx.now,
282                requesting_interfaces: Vec::new(),
283                engaged: false,
284            });
285        if !request.requesting_interfaces.contains(&ctx.interface_id) {
286            request.requesting_interfaces.push(ctx.interface_id);
287        }
288        self.discovery_path_request_deadlines
289            .entry(ctx.destination_hash)
290            .or_insert(ctx.now + timeout);
291    }
292
293    /// Record a locally generated path request, refreshing its gate timeout.
294    #[doc(hidden)]
295    pub fn record_outbound_path_request(&mut self, destination_hash: [u8; 16], now: f64) {
296        self.path_requests.insert(destination_hash, now);
297    }
298
299    fn handle_known_path_request(
300        &mut self,
301        ctx: &AcceptedPathRequest,
302        from_local_client: bool,
303    ) -> bool {
304        let Some(path) = self
305            .path_table
306            .get(&ctx.destination_hash)
307            .and_then(|ps| ps.primary())
308            .cloned()
309        else {
310            return false;
311        };
312
313        if let Some(recv_info) = self.interfaces.get(&ctx.interface_id) {
314            if recv_info.mode == constants::MODE_ROAMING
315                && path.receiving_interface == ctx.interface_id
316            {
317                return true;
318            }
319        }
320
321        let Some(raw) = path.announce_raw.as_ref() else {
322            return false;
323        };
324        // Leave the active entry in place until replacement so insertion can
325        // preserve its pending send deadline under repeated path requests.
326        if let Some(existing) = self.announce_table.get(&ctx.destination_hash).cloned() {
327            self.insert_held_announce(ctx.destination_hash, existing, ctx.now);
328        }
329        // Transport.py:3492-3506: answer local clients, and requests for
330        // destinations behind a local client, immediately.
331        let retransmit_timeout =
332            if from_local_client || self.interface_is_local_client(path.receiving_interface) {
333                ctx.now
334            } else if let Some(iface_info) = self.interfaces.get(&ctx.interface_id) {
335                let base = ctx.now + constants::PATH_REQUEST_GRACE;
336                if iface_info.mode == constants::MODE_ROAMING {
337                    base + constants::PATH_REQUEST_RG
338                } else {
339                    base
340                }
341            } else {
342                ctx.now + constants::PATH_REQUEST_GRACE
343            };
344
345        let Ok(parsed) = RawPacket::unpack(raw) else {
346            return false;
347        };
348
349        let entry = AnnounceEntry {
350            timestamp: ctx.now,
351            retransmit_timeout,
352            retries: constants::PATHFINDER_R,
353            received_from: path.next_hop,
354            hops: path.hops,
355            packet_raw: raw.clone(),
356            packet_data: parsed.data().to_vec(),
357            destination_hash: ctx.destination_hash,
358            context_flag: parsed.flags.context_flag,
359            local_rebroadcasts: 0,
360            block_rebroadcasts: true,
361            attached_interface: Some(ctx.interface_id),
362        };
363
364        self.insert_announce_entry(ctx.destination_hash, entry, ctx.now);
365        true
366    }
367
368    /// Whether a path request on `interface_id` searches for unknown paths
369    /// (`should_search_for_unknown` in Transport.py), and in which modes.
370    fn discovery_search_mode(&self, interface_id: InterfaceId) -> Option<Option<&'static [u8]>> {
371        self.interfaces
372            .get(&interface_id)
373            .and_then(|info| search_mode_filter(info.mode, info.recursive_prs))
374    }
375
376    fn handle_discovery_path_request(
377        &mut self,
378        ctx: &AcceptedPathRequest,
379        ingress_limited: bool,
380    ) -> Vec<TransportAction> {
381        let Some((mode, recursive_prs, ingress_control, ip_freq, started)) =
382            self.interfaces.get(&ctx.interface_id).map(|info| {
383                (
384                    info.mode,
385                    info.recursive_prs,
386                    info.ingress_control,
387                    info.ip_freq,
388                    info.started,
389                )
390            })
391        else {
392            return Vec::new();
393        };
394
395        let Some(search_mode_filter) = search_mode_filter(mode, recursive_prs) else {
396            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
397                "Not discovering path to {:02x?}: recursive path discovery is disabled on interface {}",
398                &ctx.destination_hash[..4],
399                ctx.interface_id.0,
400            );
401            return Vec::new();
402        };
403
404        if ingress_limited
405            || self.ingress_control.should_ingress_limit_pr(
406                ctx.interface_id,
407                &ingress_control,
408                ip_freq,
409                started,
410                ctx.now,
411            )
412        {
413            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
414                "Not discovering path to {:02x?}: ingress path-request limiting is active on interface {}",
415                &ctx.destination_hash[..4],
416                ctx.interface_id.0,
417            );
418            return Vec::new();
419        }
420
421        let egress_candidates: Vec<_> = self
422            .interfaces
423            .values()
424            .filter(|info| info.id != ctx.interface_id && info.out_capable)
425            .filter(|info| search_mode_filter.is_none_or(|modes| modes.contains(&info.mode)))
426            .map(|info| {
427                (
428                    info.id,
429                    info.ingress_control,
430                    info.op_freq,
431                    info.op_samples,
432                    info.bitrate,
433                    info.airtime_profile,
434                    info.announce_cap,
435                )
436            })
437            .collect();
438
439        let Some((path_request_raw, path_request_len)) = build_path_request_packet(
440            &ctx.destination_hash,
441            self.config.identity_hash.as_ref(),
442            &ctx.tag[..ctx.tag_len],
443        ) else {
444            return Vec::new();
445        };
446
447        let mut actions = Vec::new();
448        for (id, ingress_control, op_freq, op_samples, bitrate, airtime_profile, announce_cap) in
449            egress_candidates
450        {
451            if self.ingress_control.should_egress_limit_pr(
452                id,
453                &ingress_control,
454                op_freq,
455                op_samples,
456            ) || self
457                .announce_queues
458                .blocks_recursive_path_request(id, ctx.now)
459            {
460                continue;
461            }
462
463            self.announce_queues.reserve_recursive_path_request(
464                id,
465                path_request_len + constants::HEADER_MINSIZE,
466                ctx.now,
467                bitrate,
468                airtime_profile,
469                announce_cap,
470            );
471            actions.push(TransportAction::SendOnInterface {
472                interface: id,
473                raw: path_request_raw.clone().into(),
474            });
475        }
476
477        if !actions.is_empty() {
478            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
479                "Discovering unknown path to {:02x?} on behalf of interface {} via {} interfaces",
480                &ctx.destination_hash[..4],
481                ctx.interface_id.0,
482                actions.len(),
483            );
484            let request = self
485                .discovery_path_requests
486                .entry(ctx.destination_hash)
487                .or_insert_with(|| DiscoveryPathRequest {
488                    timestamp: ctx.now,
489                    requesting_interfaces: Vec::new(),
490                    engaged: false,
491                });
492            if !request.requesting_interfaces.contains(&ctx.interface_id) {
493                request.requesting_interfaces.push(ctx.interface_id);
494            }
495            request.engaged = true;
496            let timeout = discovery_path_request_timeout(&self.interfaces);
497            self.discovery_path_request_deadlines
498                .insert(ctx.destination_hash, ctx.now + timeout);
499        } else {
500            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
501                "Not discovering path to {:02x?}: no eligible egress interface",
502                &ctx.destination_hash[..4],
503            );
504        }
505
506        actions
507    }
508}
509
510/// Interface modes to search for an unknown path requested on an interface
511/// in `mode`: `None` if it does not search, `Some(None)` for all modes.
512fn search_mode_filter(mode: u8, recursive_prs: bool) -> Option<Option<&'static [u8]>> {
513    if recursive_prs || constants::DISCOVER_PATHS_FOR.contains(&mode) {
514        Some(None)
515    } else if mode == constants::MODE_BOUNDARY {
516        Some(Some(&constants::BOUNDARY_SEARCH_MODES))
517    } else {
518        None
519    }
520}
521
522fn build_path_request_packet(
523    destination_hash: &[u8; 16],
524    transport_identity_hash: Option<&[u8; 16]>,
525    tag: &[u8],
526) -> Option<(Vec<u8>, usize)> {
527    let mut data = Vec::with_capacity(16 + transport_identity_hash.map_or(0, |_| 16) + tag.len());
528    data.extend_from_slice(destination_hash);
529    if let Some(identity_hash) = transport_identity_hash {
530        data.extend_from_slice(identity_hash);
531    }
532    data.extend_from_slice(tag);
533
534    let flags = crate::packet::PacketFlags {
535        header_type: constants::HEADER_1,
536        context_flag: constants::FLAG_UNSET,
537        transport_type: constants::TRANSPORT_BROADCAST,
538        destination_type: constants::DESTINATION_PLAIN,
539        packet_type: constants::PACKET_TYPE_DATA,
540    };
541    let path_request_dest =
542        crate::destination::destination_hash("rnstransport", &["path", "request"], None);
543
544    let data_len = data.len();
545    RawPacket::pack(
546        flags,
547        0,
548        &path_request_dest,
549        None,
550        constants::CONTEXT_NONE,
551        &data,
552    )
553    .ok()
554    .map(|packet| (packet.raw, data_len))
555}