Skip to main content

rns_core/transport/
maintenance.rs

1use super::*;
2
3impl TransportEngine {
4    /// Periodic maintenance. Call regularly (e.g., every 250ms).
5    pub fn tick(&mut self, now: f64, rng: &mut dyn Rng) -> Vec<TransportAction> {
6        let mut ctx = TickCtx {
7            now,
8            rng,
9            actions: Vec::new(),
10        };
11        self.process_tick_pending_announces(&mut ctx);
12
13        let mut queue_actions = self.announce_queues.process_queues(now, &self.interfaces);
14        ctx.actions.append(&mut queue_actions);
15
16        self.process_tick_ingress_release(&mut ctx);
17        self.cull_tick_tables(&mut ctx);
18        self.process_tick_rediscovery(&mut ctx);
19        ctx.actions
20    }
21
22    /// Queue a path rediscovery request for `destination_hash`, skipping
23    /// `skip_interface`, unless one is already queued or the queue is full.
24    pub fn queue_path_rediscovery(
25        &mut self,
26        destination_hash: [u8; 16],
27        skip_interface: Option<InterfaceId>,
28        now: f64,
29    ) {
30        if self
31            .pending_discovery_prs
32            .iter()
33            .any(|(queued, _)| *queued == destination_hash)
34            || self.pending_discovery_prs.len() >= constants::MAX_QUEUED_DISCOVERY_PRS
35        {
36            return;
37        }
38        // Upstream's sender thread sleeps one throttle interval before each
39        // request, the first included.
40        if self.pending_discovery_prs.is_empty() {
41            self.discovery_pr_next_tx = self
42                .discovery_pr_next_tx
43                .max(now + constants::DISCOVERY_PR_TX_THROTTLE);
44        }
45        self.pending_discovery_prs
46            .push_back((destination_hash, skip_interface));
47    }
48
49    /// When this node last sent or accepted a path request for
50    /// `destination_hash`, if within `PATH_REQUEST_GATE_TIMEOUT`.
51    pub fn last_path_request(&self, destination_hash: &[u8; 16]) -> Option<f64> {
52        self.path_requests.get(destination_hash).copied()
53    }
54
55    /// `Transport.handle_disovery_path_requests`: send the oldest queued
56    /// rediscovery request once its throttle interval has passed.
57    fn process_tick_rediscovery(&mut self, ctx: &mut TickCtx<'_>) {
58        if ctx.now < self.discovery_pr_next_tx {
59            return;
60        }
61        if let Some((destination_hash, skip_interface)) = self.pending_discovery_prs.pop_front() {
62            ctx.actions.push(TransportAction::RediscoverPath {
63                destination_hash,
64                skip_interface,
65            });
66            self.discovery_pr_next_tx = ctx.now + constants::DISCOVERY_PR_TX_THROTTLE;
67        }
68    }
69
70    /// The path rediscovery half of the link table job (Transport.py
71    /// 1041-1115): a forwarded link request that was never proven may mean
72    /// the path to its destination went stale.
73    fn rediscover_unproven_link_paths(&mut self, now: f64) {
74        let mut requests: Vec<([u8; 16], Option<InterfaceId>)> = Vec::new();
75        let unproven: Vec<LinkEntry> = self
76            .link_table
77            .values()
78            .filter(|entry| !entry.validated && now > entry.proof_timeout)
79            .cloned()
80            .collect();
81        for entry in unproven {
82            let destination_hash = entry.destination_hash;
83            let throttled = self
84                .last_path_request(&destination_hash)
85                .is_some_and(|requested_at| now - requested_at < constants::PATH_REQUEST_MI);
86            let mut skip_interface = None;
87            let mut mark_unresponsive = false;
88            let rediscover = if !self.has_path(&destination_hash) {
89                // The path was dropped after the link request went out.
90                log::trace!(target: crate::logging::PATHING_LOG_TARGET,
91                    "Trying to rediscover path for {:02x?} since an attempted link was never established, and path is now missing",
92                    &destination_hash[..4],
93                );
94                true
95            } else if throttled {
96                false
97            } else if entry.taken_hops == 0 {
98                log::trace!(target: crate::logging::PATHING_LOG_TARGET,
99                    "Trying to rediscover path for {:02x?} since an attempted local client link was never established",
100                    &destination_hash[..4],
101                );
102                true
103            } else if self.hops_to(&destination_hash) == Some(1) {
104                // The destination was local to one of our interfaces and
105                // probably roamed.
106                log::trace!(target: crate::logging::PATHING_LOG_TARGET,
107                    "Trying to rediscover path for {:02x?} since an attempted link was never established, and destination was previously local to an interface on this instance",
108                    &destination_hash[..4],
109                );
110                skip_interface = Some(entry.received_interface);
111                mark_unresponsive = true;
112                true
113            } else if entry.taken_hops == 1 {
114                // The initiator is local, so the topology probably changed.
115                log::trace!(target: crate::logging::PATHING_LOG_TARGET,
116                    "Trying to rediscover path for {:02x?} since an attempted link was never established, and link initiator is local to an interface on this instance",
117                    &destination_hash[..4],
118                );
119                skip_interface = Some(entry.received_interface);
120                mark_unresponsive = true;
121                true
122            } else {
123                false
124            };
125            if !rediscover {
126                continue;
127            }
128            if mark_unresponsive && self.config.transport_enabled {
129                // Skipped when the request came in on a boundary interface.
130                self.mark_path_unresponsive(&destination_hash, Some(entry.received_interface));
131            }
132            if !requests
133                .iter()
134                .any(|(queued, _)| *queued == destination_hash)
135            {
136                requests.push((destination_hash, skip_interface));
137            }
138            if !self.config.transport_enabled {
139                // Let a non-transport node take a longer path or a path from
140                // a newly adjacent transport instance.
141                self.expire_path(&destination_hash);
142            }
143        }
144        for (destination_hash, skip_interface) in requests {
145            self.queue_path_rediscovery(destination_hash, skip_interface, now);
146        }
147    }
148
149    fn process_tick_pending_announces(&mut self, ctx: &mut TickCtx<'_>) {
150        if ctx.now <= self.announces_last_checked + constants::ANNOUNCES_CHECK_INTERVAL {
151            return;
152        }
153
154        self.cull_expired_announce_entries(ctx.now);
155        self.enforce_announce_retention_cap(ctx.now);
156        if let Some(identity_hash) = self.config.identity_hash {
157            let announce_actions = jobs::process_pending_announces(
158                &mut self.announce_table,
159                &mut self.held_announces,
160                &identity_hash,
161                ctx.now,
162            );
163            let gated = self.gate_retransmit_actions(announce_actions, ctx.now);
164            ctx.actions.extend(gated);
165        }
166        self.cull_expired_announce_entries(ctx.now);
167        self.enforce_announce_retention_cap(ctx.now);
168        self.announces_last_checked = ctx.now;
169    }
170
171    fn process_tick_ingress_release(&mut self, ctx: &mut TickCtx<'_>) {
172        let ic_interfaces = self.ingress_control.interfaces_with_held();
173        for iface_id in ic_interfaces {
174            let (ia_freq, started, ingress_config) = match self.interfaces.get(&iface_id) {
175                Some(info) => (info.ia_freq, info.started, info.ingress_control),
176                None => continue,
177            };
178            if !ingress_config.enabled {
179                continue;
180            }
181            if let Some(held) = self.ingress_control.process_held_announces(
182                iface_id,
183                &ingress_config,
184                ia_freq,
185                started,
186                ctx.now,
187            ) {
188                let released_actions = self.handle_inbound(
189                    InboundFrame {
190                        raw: &held.raw,
191                        iface: held.receiving_interface,
192                        now: ctx.now,
193                        rx: held.rx,
194                    },
195                    ctx.rng,
196                );
197                ctx.actions.extend(released_actions);
198            }
199        }
200    }
201
202    fn cull_tick_tables(&mut self, ctx: &mut TickCtx<'_>) {
203        if ctx.now <= self.tables_last_culled + constants::TABLES_CULL_INTERVAL {
204            return;
205        }
206
207        let unused_path_linger = self.unused_path_linger();
208        jobs::cull_path_table(
209            &mut self.path_table,
210            &self.interfaces,
211            ctx.now,
212            unused_path_linger,
213            constants::PATH_POOL_UNUSED_TARGET,
214        );
215        jobs::cull_reverse_table(&mut self.reverse_table, &self.interfaces, ctx.now);
216        self.rediscover_unproven_link_paths(ctx.now);
217        let (_culled, link_closed_actions) =
218            jobs::cull_link_table(&mut self.link_table, &self.interfaces, ctx.now);
219        ctx.actions.extend(link_closed_actions);
220        jobs::cull_path_states(&mut self.path_states, &self.path_table);
221        self.cull_blackholed(ctx.now);
222        self.path_requests.retain(|_, requested_at| {
223            ctx.now < *requested_at + constants::PATH_REQUEST_GATE_TIMEOUT
224        });
225        self.discovery_path_requests.retain(|destination, req| {
226            let deadline = self
227                .discovery_path_request_deadlines
228                .get(destination)
229                .copied()
230                .unwrap_or(req.timestamp + constants::DISCOVERY_PATH_REQUEST_TIMEOUT);
231            ctx.now < deadline
232        });
233        self.discovery_path_request_deadlines
234            .retain(|destination, _| self.discovery_path_requests.contains_key(destination));
235        // Transport.py culls these only when the requesting interface is
236        // gone; also expire them, as a local client answers right away.
237        self.pending_local_path_requests
238            .retain(|_, (interface, requested_at)| {
239                self.interfaces.contains_key(interface)
240                    && ctx.now < *requested_at + constants::PATH_REQUEST_TIMEOUT
241            });
242        self.tunnel_table
243            .void_missing_interfaces(|id| self.interfaces.contains_key(id));
244        self.tunnel_table.cull(ctx.now);
245        self.announce_sig_cache.cull(ctx.now);
246        self.tables_last_culled = ctx.now;
247    }
248
249    /// Gate retransmitted announce actions through per-interface bandwidth queues.
250    ///
251    /// Retransmitted announces always have hops > 0.
252    /// `BroadcastOnAllInterfaces` is expanded to per-interface sends gated through queues.
253    pub(super) fn gate_retransmit_actions(
254        &mut self,
255        actions: Vec<TransportAction>,
256        now: f64,
257    ) -> Vec<TransportAction> {
258        let mut result = Vec::new();
259        for action in actions {
260            match action {
261                TransportAction::SendOnInterface { interface, raw } => {
262                    // Extract dest_hash from raw (bytes 2..18 for H1, 18..34 for H2)
263                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
264                    let (bitrate, airtime_profile, announce_cap) =
265                        if let Some(info) = self.interfaces.get(&interface) {
266                            (info.bitrate, info.airtime_profile, info.announce_cap)
267                        } else {
268                            (None, None, constants::ANNOUNCE_CAP)
269                        };
270                    if let Some(send_action) = self.announce_queues.gate_announce(
271                        interface,
272                        raw,
273                        dest_hash,
274                        hops,
275                        now,
276                        now,
277                        bitrate,
278                        airtime_profile,
279                        announce_cap,
280                    ) {
281                        result.push(send_action);
282                    }
283                }
284                TransportAction::BroadcastOnAllInterfaces { raw, exclude } => {
285                    let (dest_hash, hops) = Self::extract_announce_info(&raw);
286                    // Expand to per-interface sends gated through queues,
287                    // applying mode filtering (AP blocks non-local announces, etc.)
288                    let iface_ids: Vec<(
289                        InterfaceId,
290                        Option<u64>,
291                        Option<types::AirtimeProfile>,
292                        f64,
293                    )> = self
294                        .interfaces
295                        .iter()
296                        .filter(|(_, info)| info.out_capable)
297                        // Without transport, the only rebroadcasts are local
298                        // client announces, which the local client relay has
299                        // already delivered to the other local clients.
300                        .filter(|(_, info)| self.config.transport_enabled || !info.is_local_client)
301                        .filter(|(id, _)| {
302                            if let Some(ref ex) = exclude {
303                                **id != *ex
304                            } else {
305                                true
306                            }
307                        })
308                        .filter(|(_, info)| {
309                            should_transmit_announce(
310                                info,
311                                &dest_hash,
312                                hops,
313                                &self.local_destinations,
314                                &self.path_table,
315                                &self.interfaces,
316                            )
317                        })
318                        .map(|(id, info)| {
319                            (*id, info.bitrate, info.airtime_profile, info.announce_cap)
320                        })
321                        .collect();
322
323                    for (iface_id, bitrate, airtime_profile, announce_cap) in iface_ids {
324                        if let Some(send_action) = self.announce_queues.gate_announce(
325                            iface_id,
326                            raw.clone(),
327                            dest_hash,
328                            hops,
329                            now,
330                            now,
331                            bitrate,
332                            airtime_profile,
333                            announce_cap,
334                        ) {
335                            result.push(send_action);
336                        }
337                    }
338                }
339                other => result.push(other),
340            }
341        }
342        result
343    }
344
345    /// Extract destination hash and hops from raw announce bytes.
346    fn extract_announce_info(raw: &[u8]) -> ([u8; 16], u8) {
347        if raw.len() < 18 {
348            return ([0; 16], 0);
349        }
350        let header_type = (raw[0] >> 6) & 0x03;
351        let hops = raw[1];
352        if header_type == constants::HEADER_2 && raw.len() >= 34 {
353            // H2: transport_id at [2..18], dest_hash at [18..34]
354            let mut dest = [0u8; 16];
355            dest.copy_from_slice(&raw[18..34]);
356            (dest, hops)
357        } else {
358            // H1: dest_hash at [2..18]
359            let mut dest = [0u8; 16];
360            dest.copy_from_slice(&raw[2..18]);
361            (dest, hops)
362        }
363    }
364
365    #[cfg(test)]
366    #[allow(dead_code)]
367    pub(crate) fn link_table_ref(&self) -> &BTreeMap<[u8; 16], LinkEntry> {
368        &self.link_table
369    }
370}