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