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        if self.local_destinations.contains_key(&ctx.destination_hash) {
101            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
102                "Ignoring path request for {:02x?}: destination is local",
103                &ctx.destination_hash[..4],
104            );
105            return Vec::new();
106        }
107        if self.config.transport_enabled && self.handle_known_path_request(&ctx) {
108            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
109                "Answering path request for {:02x?}: path is known",
110                &ctx.destination_hash[..4],
111            );
112            return Vec::new();
113        }
114        if self.config.transport_enabled {
115            return self.handle_discovery_path_request(&ctx, ingress_limited);
116        }
117        log::trace!(target: crate::logging::PATHING_LOG_TARGET,
118            "Ignoring path request for {:02x?}: transport is disabled",
119            &ctx.destination_hash[..4],
120        );
121        Vec::new()
122    }
123
124    fn parse_path_request(
125        &mut self,
126        data: &[u8],
127        interface_id: InterfaceId,
128        now: f64,
129    ) -> Option<AcceptedPathRequest> {
130        if data.len() < 16 {
131            return None;
132        }
133
134        let mut destination_hash = [0u8; 16];
135        destination_hash.copy_from_slice(&data[..16]);
136
137        let tag_bytes = if data.len() > 32 {
138            Some(&data[32..])
139        } else if data.len() > 16 {
140            Some(&data[16..])
141        } else {
142            None
143        };
144        let Some(tag_bytes) = tag_bytes else {
145            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
146                "Ignoring tagless path request for {:02x?}",
147                &destination_hash[..4],
148            );
149            return None;
150        };
151
152        let tag_len = tag_bytes.len().min(16);
153        let mut unique_tag = [0u8; 32];
154        unique_tag[..16].copy_from_slice(&destination_hash);
155        unique_tag[16..16 + tag_len].copy_from_slice(&tag_bytes[..tag_len]);
156        if !self.insert_discovery_pr_tag(unique_tag) {
157            return None;
158        }
159
160        let already_in_flight = self.path_requests.contains_key(&destination_hash)
161            && !self.path_table.contains_key(&destination_hash)
162            && !self.local_destinations.contains_key(&destination_hash);
163        self.path_requests.entry(destination_hash).or_insert(now);
164
165        let mut tag = [0u8; 16];
166        tag[..tag_len].copy_from_slice(&tag_bytes[..tag_len]);
167        Some(AcceptedPathRequest {
168            tag,
169            tag_len,
170            interface_id,
171            now,
172            destination_hash,
173            already_in_flight,
174        })
175    }
176
177    fn batch_inflight_path_request(&mut self, ctx: &AcceptedPathRequest, ingress_limited: bool) {
178        let Some((ingress_control, ip_freq, started)) = self
179            .interfaces
180            .get(&ctx.interface_id)
181            .map(|info| (info.ingress_control, info.ip_freq, info.started))
182        else {
183            return;
184        };
185        if ingress_limited
186            || self.ingress_control.should_ingress_limit_pr(
187                ctx.interface_id,
188                &ingress_control,
189                ip_freq,
190                started,
191                ctx.now,
192            )
193        {
194            return;
195        }
196
197        let timeout = discovery_path_request_timeout(&self.interfaces);
198        let request = self
199            .discovery_path_requests
200            .entry(ctx.destination_hash)
201            .or_insert_with(|| DiscoveryPathRequest {
202                timestamp: ctx.now,
203                requesting_interfaces: Vec::new(),
204                engaged: false,
205            });
206        if !request.requesting_interfaces.contains(&ctx.interface_id) {
207            request.requesting_interfaces.push(ctx.interface_id);
208        }
209        self.discovery_path_request_deadlines
210            .entry(ctx.destination_hash)
211            .or_insert(ctx.now + timeout);
212    }
213
214    /// Record a locally generated path request, refreshing its gate timeout.
215    #[doc(hidden)]
216    pub fn record_outbound_path_request(&mut self, destination_hash: [u8; 16], now: f64) {
217        self.path_requests.insert(destination_hash, now);
218    }
219
220    fn handle_known_path_request(&mut self, ctx: &AcceptedPathRequest) -> bool {
221        let Some(path) = self
222            .path_table
223            .get(&ctx.destination_hash)
224            .and_then(|ps| ps.primary())
225            .cloned()
226        else {
227            return false;
228        };
229
230        if let Some(recv_info) = self.interfaces.get(&ctx.interface_id) {
231            if recv_info.mode == constants::MODE_ROAMING
232                && path.receiving_interface == ctx.interface_id
233            {
234                return true;
235            }
236        }
237
238        let Some(raw) = path.announce_raw.as_ref() else {
239            return false;
240        };
241        // Leave the active entry in place until replacement so insertion can
242        // preserve its pending send deadline under repeated path requests.
243        if let Some(existing) = self.announce_table.get(&ctx.destination_hash).cloned() {
244            self.insert_held_announce(ctx.destination_hash, existing, ctx.now);
245        }
246        let retransmit_timeout = if let Some(iface_info) = self.interfaces.get(&ctx.interface_id) {
247            let base = ctx.now + constants::PATH_REQUEST_GRACE;
248            if iface_info.mode == constants::MODE_ROAMING {
249                base + constants::PATH_REQUEST_RG
250            } else {
251                base
252            }
253        } else {
254            ctx.now + constants::PATH_REQUEST_GRACE
255        };
256
257        let Ok(parsed) = RawPacket::unpack(raw) else {
258            return false;
259        };
260
261        let entry = AnnounceEntry {
262            timestamp: ctx.now,
263            retransmit_timeout,
264            retries: constants::PATHFINDER_R,
265            received_from: path.next_hop,
266            hops: path.hops,
267            packet_raw: raw.clone(),
268            packet_data: parsed.data().to_vec(),
269            destination_hash: ctx.destination_hash,
270            context_flag: parsed.flags.context_flag,
271            local_rebroadcasts: 0,
272            block_rebroadcasts: true,
273            attached_interface: Some(ctx.interface_id),
274        };
275
276        self.insert_announce_entry(ctx.destination_hash, entry, ctx.now);
277        true
278    }
279
280    fn handle_discovery_path_request(
281        &mut self,
282        ctx: &AcceptedPathRequest,
283        ingress_limited: bool,
284    ) -> Vec<TransportAction> {
285        let Some((mode, recursive_prs, ingress_control, ip_freq, started)) =
286            self.interfaces.get(&ctx.interface_id).map(|info| {
287                (
288                    info.mode,
289                    info.recursive_prs,
290                    info.ingress_control,
291                    info.ip_freq,
292                    info.started,
293                )
294            })
295        else {
296            return Vec::new();
297        };
298
299        let search_mode_filter: Option<&[u8]> = if recursive_prs
300            || constants::DISCOVER_PATHS_FOR.contains(&mode)
301        {
302            None
303        } else if mode == constants::MODE_BOUNDARY {
304            Some(&constants::BOUNDARY_SEARCH_MODES)
305        } else {
306            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
307                "Not discovering path to {:02x?}: recursive path discovery is disabled on interface {}",
308                &ctx.destination_hash[..4],
309                ctx.interface_id.0,
310            );
311            return Vec::new();
312        };
313
314        if ingress_limited
315            || self.ingress_control.should_ingress_limit_pr(
316                ctx.interface_id,
317                &ingress_control,
318                ip_freq,
319                started,
320                ctx.now,
321            )
322        {
323            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
324                "Not discovering path to {:02x?}: ingress path-request limiting is active on interface {}",
325                &ctx.destination_hash[..4],
326                ctx.interface_id.0,
327            );
328            return Vec::new();
329        }
330
331        let egress_candidates: Vec<_> = self
332            .interfaces
333            .values()
334            .filter(|info| info.id != ctx.interface_id && info.out_capable)
335            .filter(|info| search_mode_filter.is_none_or(|modes| modes.contains(&info.mode)))
336            .map(|info| {
337                (
338                    info.id,
339                    info.ingress_control,
340                    info.op_freq,
341                    info.op_samples,
342                    info.bitrate,
343                    info.airtime_profile,
344                    info.announce_cap,
345                )
346            })
347            .collect();
348
349        let Some((path_request_raw, path_request_len)) = build_path_request_packet(
350            &ctx.destination_hash,
351            self.config.identity_hash.as_ref(),
352            &ctx.tag[..ctx.tag_len],
353        ) else {
354            return Vec::new();
355        };
356
357        let mut actions = Vec::new();
358        for (id, ingress_control, op_freq, op_samples, bitrate, airtime_profile, announce_cap) in
359            egress_candidates
360        {
361            if self.ingress_control.should_egress_limit_pr(
362                id,
363                &ingress_control,
364                op_freq,
365                op_samples,
366            ) || self
367                .announce_queues
368                .blocks_recursive_path_request(id, ctx.now)
369            {
370                continue;
371            }
372
373            self.announce_queues.reserve_recursive_path_request(
374                id,
375                path_request_len + constants::HEADER_MINSIZE,
376                ctx.now,
377                bitrate,
378                airtime_profile,
379                announce_cap,
380            );
381            actions.push(TransportAction::SendOnInterface {
382                interface: id,
383                raw: path_request_raw.clone().into(),
384            });
385        }
386
387        if !actions.is_empty() {
388            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
389                "Discovering unknown path to {:02x?} on behalf of interface {} via {} interfaces",
390                &ctx.destination_hash[..4],
391                ctx.interface_id.0,
392                actions.len(),
393            );
394            let request = self
395                .discovery_path_requests
396                .entry(ctx.destination_hash)
397                .or_insert_with(|| DiscoveryPathRequest {
398                    timestamp: ctx.now,
399                    requesting_interfaces: Vec::new(),
400                    engaged: false,
401                });
402            if !request.requesting_interfaces.contains(&ctx.interface_id) {
403                request.requesting_interfaces.push(ctx.interface_id);
404            }
405            request.engaged = true;
406            let timeout = discovery_path_request_timeout(&self.interfaces);
407            self.discovery_path_request_deadlines
408                .insert(ctx.destination_hash, ctx.now + timeout);
409        } else {
410            log::trace!(target: crate::logging::PATHING_LOG_TARGET,
411                "Not discovering path to {:02x?}: no eligible egress interface",
412                &ctx.destination_hash[..4],
413            );
414        }
415
416        actions
417    }
418}
419
420fn build_path_request_packet(
421    destination_hash: &[u8; 16],
422    transport_identity_hash: Option<&[u8; 16]>,
423    tag: &[u8],
424) -> Option<(Vec<u8>, usize)> {
425    let mut data = Vec::with_capacity(16 + transport_identity_hash.map_or(0, |_| 16) + tag.len());
426    data.extend_from_slice(destination_hash);
427    if let Some(identity_hash) = transport_identity_hash {
428        data.extend_from_slice(identity_hash);
429    }
430    data.extend_from_slice(tag);
431
432    let flags = crate::packet::PacketFlags {
433        header_type: constants::HEADER_1,
434        context_flag: constants::FLAG_UNSET,
435        transport_type: constants::TRANSPORT_BROADCAST,
436        destination_type: constants::DESTINATION_PLAIN,
437        packet_type: constants::PACKET_TYPE_DATA,
438    };
439    let path_request_dest =
440        crate::destination::destination_hash("rnstransport", &["path", "request"], None);
441
442    let data_len = data.len();
443    RawPacket::pack(
444        flags,
445        0,
446        &path_request_dest,
447        None,
448        constants::CONTEXT_NONE,
449        &data,
450    )
451    .ok()
452    .map(|packet| (packet.raw, data_len))
453}