Skip to main content

rns_net/driver/
queries.rs

1use super::*;
2
3fn traffic_detail(stats: &crate::interface::InterfaceStats) -> TrafficDetail {
4    TrafficDetail {
5        rxs: stats.traffic_rates.rxs,
6        txs: stats.traffic_rates.txs,
7        arxb: stats.arxb,
8        atxb: stats.atxb,
9        arxc: stats.arxc,
10        atxc: stats.atxc,
11        arxs: stats.traffic_rates.arxs,
12        atxs: stats.traffic_rates.atxs,
13        prxb: stats.prxb,
14        ptxb: stats.ptxb,
15        prxc: stats.prxc,
16        ptxc: stats.ptxc,
17        prxs: stats.traffic_rates.prxs,
18        ptxs: stats.traffic_rates.ptxs,
19        arxf: stats.incoming_announce_freq(),
20        atxf: stats.outgoing_announce_freq(),
21        prxf: stats.incoming_path_request_freq(),
22        ptxf: stats.outgoing_path_request_freq(),
23    }
24}
25
26fn add_traffic(total: &mut TrafficDetail, detail: TrafficDetail) {
27    total.rxs += detail.rxs;
28    total.txs += detail.txs;
29    total.arxb += detail.arxb;
30    total.atxb += detail.atxb;
31    total.arxc += detail.arxc;
32    total.atxc += detail.atxc;
33    total.arxs += detail.arxs;
34    total.atxs += detail.atxs;
35    total.prxb += detail.prxb;
36    total.ptxb += detail.ptxb;
37    total.prxc += detail.prxc;
38    total.ptxc += detail.ptxc;
39    total.prxs += detail.prxs;
40    total.ptxs += detail.ptxs;
41    total.arxf += detail.arxf;
42    total.atxf += detail.atxf;
43    total.prxf += detail.prxf;
44    total.ptxf += detail.ptxf;
45}
46
47impl Driver {
48    pub(super) fn dynamic_burst_counts(&self, parent_id: InterfaceId) -> (usize, usize) {
49        self.dynamic_interface_parents
50            .iter()
51            .filter(|(_, candidate_parent)| **candidate_parent == parent_id)
52            .fold((0, 0), |(announce_count, path_request_count), (id, _)| {
53                (
54                    announce_count + usize::from(self.engine.burst_active(id)),
55                    path_request_count + usize::from(self.engine.pr_burst_active(id)),
56                )
57            })
58    }
59
60    /// Return active announce and path-request burst counts for a Backbone listener.
61    #[cfg(feature = "iface-backbone")]
62    pub fn backbone_burst_counts(&self, parent_id: InterfaceId) -> Option<(usize, usize)> {
63        self.backbone_peer_state
64            .values()
65            .any(|handle| handle.interface_id == parent_id)
66            .then(|| self.dynamic_burst_counts(parent_id))
67    }
68
69    pub(crate) fn handle_interface_stats_query(&self) -> QueryResponse {
70        let now = time::now();
71        let mut interfaces = Vec::new();
72        let mut total_rxb: u64 = 0;
73        let mut total_txb: u64 = 0;
74        let mut rx_pps = 0.0;
75        let mut tx_pps = 0.0;
76        let mut total_traffic = TrafficDetail::default();
77        for entry in self.interfaces.values() {
78            let traffic = traffic_detail(&entry.stats);
79            if !entry.info.is_local_client {
80                total_rxb += entry.stats.rxb;
81                total_txb += entry.stats.txb;
82                rx_pps += entry.stats.traffic_rates.rxpps;
83                tx_pps += entry.stats.traffic_rates.txpps;
84                add_traffic(&mut total_traffic, traffic);
85            }
86            let interface_id = entry.info.id;
87            interfaces.push(SingleInterfaceStat {
88                id: interface_id.0,
89                name: entry.info.name.clone(),
90                status: entry.online && entry.enabled,
91                mode: entry.info.mode,
92                gravity: entry.info.gravity,
93                announces_to_internal: entry.info.announces_to_internal,
94                rxb: entry.stats.rxb,
95                txb: entry.stats.txb,
96                tx_drops: entry
97                    .async_writer_metrics
98                    .as_ref()
99                    .map(crate::interface::AsyncWriterMetrics::tx_drops)
100                    .unwrap_or(0),
101                tx_dropped_bytes: entry
102                    .async_writer_metrics
103                    .as_ref()
104                    .map(crate::interface::AsyncWriterMetrics::tx_dropped_bytes)
105                    .unwrap_or(0),
106                tx_stalled: entry
107                    .async_writer_metrics
108                    .as_ref()
109                    .is_some_and(crate::interface::AsyncWriterMetrics::tx_stalled),
110                tx_buffered: entry
111                    .async_writer_metrics
112                    .as_ref()
113                    .map(crate::interface::AsyncWriterMetrics::tx_buffered)
114                    .unwrap_or(0),
115                traffic,
116                protocol_violations: entry.stats.protocol_violations,
117                ifac_violations: entry.stats.ifac_violations,
118                packet_filter_hits: entry.stats.packet_filter_hits,
119                rx_packets: entry.stats.rx_packets,
120                tx_packets: entry.stats.tx_packets,
121                cpu_load: entry.stats.cpu_load,
122                mem_load: entry.stats.mem_load,
123                switch_id: entry.stats.switch_id,
124                endpoint_id: entry.stats.endpoint_id,
125                via_switch_id: entry.stats.via_switch_id,
126                peers: entry.stats.peers,
127                bitrate: entry.info.bitrate,
128                mtu: Some(entry.info.mtu),
129                ifac_size: entry.ifac.as_ref().map(|s| s.size),
130                started: entry.stats.started,
131                ia_freq: entry.stats.incoming_announce_freq(),
132                oa_freq: entry.stats.outgoing_announce_freq(),
133                ip_freq: entry.stats.incoming_path_request_freq(),
134                op_freq: entry.stats.outgoing_path_request_freq(),
135                op_samples: entry.stats.outgoing_path_request_samples(),
136                burst_active: self.engine.burst_active(&interface_id),
137                burst_activated: self.engine.burst_activated(&interface_id),
138                pr_burst_active: self.engine.pr_burst_active(&interface_id),
139                pr_burst_activated: self.engine.pr_burst_activated(&interface_id),
140                clients: None,
141                blocked_ips: None,
142                blocked_ip_list: None,
143                announce_rate_target: entry.info.announce_rate_target,
144                announce_rate_grace: entry.info.announce_rate_grace,
145                announce_rate_penalty: entry.info.announce_rate_penalty,
146                interface_type: entry.interface_type.clone(),
147            });
148        }
149        #[cfg(feature = "iface-backbone")]
150        for handle in self.backbone_peer_state.values() {
151            let clients = recover_mutex_guard(&handle.peer_state, "backbone peer state")
152                .list(&handle.interface_name)
153                .into_iter()
154                .map(|entry| entry.connected_count as u64)
155                .sum();
156            let blocked_ip_list =
157                recover_mutex_guard(&handle.fast_flap_state, "backbone fast-flap state")
158                    .blocked_ip_list(&handle.fast_flap, std::time::Instant::now())
159                    .into_iter()
160                    .map(|peer_ip| peer_ip.to_string())
161                    .collect::<Vec<_>>();
162            let blocked_ips = blocked_ip_list.len() as u64;
163            let mut traffic = TrafficDetail::default();
164            let mut protocol_violations = 0;
165            let mut ifac_violations = 0;
166            let mut packet_filter_hits = 0;
167            let mut tx_drops = 0;
168            let mut tx_dropped_bytes = 0;
169            let mut tx_stalled = false;
170            let mut tx_buffered = 0;
171            for (child_id, parent_id) in &self.dynamic_interface_parents {
172                if *parent_id == handle.interface_id {
173                    if let Some(child) = self.interfaces.get(child_id) {
174                        add_traffic(&mut traffic, traffic_detail(&child.stats));
175                        protocol_violations += child.stats.protocol_violations;
176                        ifac_violations += child.stats.ifac_violations;
177                        packet_filter_hits += child.stats.packet_filter_hits;
178                        if let Some(metrics) = &child.async_writer_metrics {
179                            tx_drops += metrics.tx_drops();
180                            tx_dropped_bytes += metrics.tx_dropped_bytes();
181                            tx_stalled |= metrics.tx_stalled();
182                            tx_buffered += metrics.tx_buffered();
183                        }
184                    }
185                }
186            }
187            interfaces.push(SingleInterfaceStat {
188                id: handle.interface_id.0,
189                name: handle.interface_name.clone(),
190                status: true,
191                mode: handle.mode,
192                gravity: handle.gravity,
193                announces_to_internal: handle.announces_to_internal,
194                rxb: 0,
195                txb: 0,
196                tx_drops,
197                tx_dropped_bytes,
198                tx_stalled,
199                tx_buffered,
200                traffic,
201                protocol_violations,
202                ifac_violations,
203                packet_filter_hits,
204                rx_packets: 0,
205                tx_packets: 0,
206                cpu_load: None,
207                mem_load: None,
208                switch_id: None,
209                endpoint_id: None,
210                via_switch_id: None,
211                peers: None,
212                bitrate: Some(crate::interface::backbone::SERVER_BITRATE_GUESS),
213                mtu: Some(crate::interface::backbone::HW_MTU as u32),
214                ifac_size: None,
215                started: self.started,
216                ia_freq: 0.0,
217                oa_freq: 0.0,
218                ip_freq: 0.0,
219                op_freq: 0.0,
220                op_samples: 0,
221                burst_active: false,
222                burst_activated: 0.0,
223                pr_burst_active: false,
224                pr_burst_activated: 0.0,
225                clients: Some(clients),
226                blocked_ips: Some(blocked_ips),
227                blocked_ip_list: Some(blocked_ip_list),
228                announce_rate_target: None,
229                announce_rate_grace: 0,
230                announce_rate_penalty: 0.0,
231                interface_type: "BackboneInterface".into(),
232            });
233        }
234        interfaces.sort_by(|a, b| a.name.cmp(&b.name));
235        QueryResponse::InterfaceStats(InterfaceStatsResponse {
236            interfaces,
237            transport_id: self.engine.identity_hash().copied(),
238            transport_enabled: self.engine.transport_enabled(),
239            transport_uptime: now - self.started,
240            total_rxb,
241            total_txb,
242            rx_pps,
243            tx_pps,
244            traffic: total_traffic,
245            probe_responder: self.probe_responder_hash,
246            #[cfg(feature = "iface-backbone")]
247            backbone_peer_pool: self.backbone_peer_pool_status(),
248            #[cfg(not(feature = "iface-backbone"))]
249            backbone_peer_pool: None,
250        })
251    }
252
253    pub(crate) fn handle_path_table_query(&self, max_hops: Option<u8>) -> QueryResponse {
254        let entries: Vec<PathTableEntry> = self
255            .engine
256            .path_table_entries()
257            .filter(|(_, entry)| max_hops.is_none_or(|max| entry.hops <= max))
258            .map(|(hash, entry)| {
259                let iface_name = self
260                    .interfaces
261                    .get(&entry.receiving_interface)
262                    .map(|e| e.info.name.clone())
263                    .or_else(|| {
264                        self.engine
265                            .interface_info(&entry.receiving_interface)
266                            .map(|i| i.name.clone())
267                    })
268                    .unwrap_or_default();
269                PathTableEntry {
270                    hash: *hash,
271                    timestamp: entry.timestamp,
272                    via: entry.next_hop,
273                    hops: entry.hops,
274                    expires: entry.expires,
275                    interface: entry.receiving_interface,
276                    interface_name: iface_name,
277                }
278            })
279            .collect();
280        QueryResponse::PathTable(entries)
281    }
282
283    pub(crate) fn handle_runtime_config_query(
284        &self,
285        request: QueryRequest,
286    ) -> Option<QueryResponse> {
287        match request {
288            QueryRequest::ListRuntimeConfig => {
289                Some(QueryResponse::RuntimeConfigList(self.list_runtime_config()))
290            }
291            QueryRequest::GetRuntimeConfig { key } => Some(QueryResponse::RuntimeConfigEntry(
292                self.runtime_config_entry(&key),
293            )),
294            QueryRequest::BackbonePeerState { interface_name } => {
295                Some(QueryResponse::BackbonePeerState(
296                    self.list_backbone_peer_state(interface_name.as_deref()),
297                ))
298            }
299            QueryRequest::SetRuntimeConfig { .. } => {
300                Some(QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
301                    code: RuntimeConfigErrorCode::Unsupported,
302                    message: "mutating runtime config is handled separately".to_string(),
303                })))
304            }
305            QueryRequest::ResetRuntimeConfig { .. } => {
306                Some(QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
307                    code: RuntimeConfigErrorCode::Unsupported,
308                    message: "mutating runtime config is handled separately".to_string(),
309                })))
310            }
311            QueryRequest::ClearBackbonePeerState { .. } => {
312                Some(QueryResponse::ClearBackbonePeerState(false))
313            }
314            QueryRequest::BlacklistBackbonePeer { .. } => {
315                Some(QueryResponse::BlacklistBackbonePeer(false))
316            }
317            _ => None,
318        }
319    }
320
321    pub(crate) fn handle_mutation_query(&mut self, request: QueryRequest) -> Option<QueryResponse> {
322        match request {
323            QueryRequest::BlackholeIdentity {
324                identity_hash,
325                duration_hours,
326                reason,
327            } => {
328                let now = time::now();
329                self.engine
330                    .blackhole_identity(identity_hash, now, duration_hours, reason);
331                Some(QueryResponse::BlackholeResult(true))
332            }
333            QueryRequest::UnblackholeIdentity { identity_hash } => Some(
334                QueryResponse::UnblackholeResult(self.engine.unblackhole_identity(&identity_hash)),
335            ),
336            QueryRequest::DropPath { dest_hash } => {
337                Some(QueryResponse::DropPath(self.engine.drop_path(&dest_hash)))
338            }
339            QueryRequest::DropAllVia { transport_hash } => Some(QueryResponse::DropAllVia(
340                self.engine.drop_all_via(&transport_hash),
341            )),
342            QueryRequest::DropAnnounceQueues => {
343                self.engine.drop_announce_queues();
344                Some(QueryResponse::DropAnnounceQueues)
345            }
346            QueryRequest::ClearBackbonePeerState {
347                interface_name,
348                peer_ip,
349            } => Some(QueryResponse::ClearBackbonePeerState(
350                self.clear_backbone_peer_state(&interface_name, peer_ip),
351            )),
352            QueryRequest::BlacklistBackbonePeer {
353                interface_name,
354                peer_ip,
355                duration,
356                reason,
357                penalty_level,
358            } => Some(QueryResponse::BlacklistBackbonePeer(
359                self.blacklist_backbone_peer(
360                    &interface_name,
361                    peer_ip,
362                    duration,
363                    reason,
364                    penalty_level,
365                ),
366            )),
367            QueryRequest::InjectPath {
368                dest_hash,
369                next_hop,
370                hops,
371                expires,
372                interface_name,
373                packet_hash,
374            } => {
375                let iface_id = self
376                    .interfaces
377                    .iter()
378                    .find(|(_, entry)| entry.info.name == interface_name)
379                    .map(|(id, _)| *id);
380                Some(match iface_id {
381                    Some(id) => {
382                        let entry = PathEntry {
383                            timestamp: time::now(),
384                            next_hop,
385                            hops,
386                            expires,
387                            random_blobs: Vec::new(),
388                            receiving_interface: id,
389                            packet_hash,
390                            announce_raw: None,
391                        };
392                        self.engine.inject_path(dest_hash, entry);
393                        QueryResponse::InjectPath(true)
394                    }
395                    None => QueryResponse::InjectPath(false),
396                })
397            }
398            QueryRequest::InjectIdentity {
399                dest_hash,
400                identity_hash,
401                public_key,
402                app_data,
403                hops,
404                received_at,
405            } => {
406                self.upsert_known_destination(
407                    dest_hash,
408                    crate::destination::AnnouncedIdentity {
409                        dest_hash: rns_core::types::DestHash(dest_hash),
410                        identity_hash: rns_core::types::IdentityHash(identity_hash),
411                        public_key,
412                        app_data,
413                        hops,
414                        received_at,
415                        receiving_interface: rns_core::transport::types::InterfaceId(0),
416                        rssi: None,
417                        snr: None,
418                    },
419                );
420                Some(QueryResponse::InjectIdentity(true))
421            }
422            QueryRequest::RestoreKnownDestination(entry) => {
423                self.known_destinations.insert(
424                    entry.dest_hash,
425                    KnownDestinationState {
426                        announced: crate::destination::AnnouncedIdentity {
427                            dest_hash: rns_core::types::DestHash(entry.dest_hash),
428                            identity_hash: rns_core::types::IdentityHash(entry.identity_hash),
429                            public_key: entry.public_key,
430                            app_data: entry.app_data,
431                            hops: entry.hops,
432                            received_at: entry.received_at,
433                            receiving_interface: entry.receiving_interface,
434                            rssi: None,
435                            snr: None,
436                        },
437                        was_used: entry.was_used,
438                        last_used_at: entry.last_used_at,
439                        retained: entry.retained,
440                    },
441                );
442                Some(QueryResponse::RestoreKnownDestination(true))
443            }
444            QueryRequest::RetainKnownDestination { dest_hash } => Some(
445                QueryResponse::RetainKnownDestination(self.retain_known_destination(&dest_hash)),
446            ),
447            QueryRequest::RetainIdentity { identity_hash } => Some(QueryResponse::RetainIdentity(
448                self.retain_identity(&identity_hash),
449            )),
450            QueryRequest::UnretainKnownDestination { dest_hash } => {
451                Some(QueryResponse::UnretainKnownDestination(
452                    self.unretain_known_destination(&dest_hash),
453                ))
454            }
455            QueryRequest::MarkKnownDestinationUsed { dest_hash } => {
456                Some(QueryResponse::MarkKnownDestinationUsed(
457                    self.mark_known_destination_used(&dest_hash),
458                ))
459            }
460            _ => None,
461        }
462    }
463
464    pub(crate) fn runtime_config_query_fallback(request: &QueryRequest) -> QueryResponse {
465        match request {
466            QueryRequest::ListRuntimeConfig => QueryResponse::RuntimeConfigList(Vec::new()),
467            QueryRequest::GetRuntimeConfig { .. } => QueryResponse::RuntimeConfigEntry(None),
468            QueryRequest::BackbonePeerState { .. } => QueryResponse::BackbonePeerState(Vec::new()),
469            QueryRequest::SetRuntimeConfig { key, .. } => {
470                QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
471                    code: RuntimeConfigErrorCode::ApplyFailed,
472                    message: format!(
473                        "internal error: no response generated for runtime-config set '{}'",
474                        key
475                    ),
476                }))
477            }
478            QueryRequest::ResetRuntimeConfig { key } => {
479                QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
480                    code: RuntimeConfigErrorCode::ApplyFailed,
481                    message: format!(
482                        "internal error: no response generated for runtime-config reset '{}'",
483                        key
484                    ),
485                }))
486            }
487            QueryRequest::ClearBackbonePeerState { .. } => {
488                QueryResponse::ClearBackbonePeerState(false)
489            }
490            QueryRequest::BlacklistBackbonePeer { .. } => {
491                QueryResponse::BlacklistBackbonePeer(false)
492            }
493            _ => QueryResponse::RuntimeConfigEntry(None),
494        }
495    }
496
497    pub(crate) fn mutation_query_fallback(request: &QueryRequest) -> QueryResponse {
498        match request {
499            QueryRequest::BlackholeIdentity { .. } => QueryResponse::BlackholeResult(false),
500            QueryRequest::UnblackholeIdentity { .. } => QueryResponse::UnblackholeResult(false),
501            QueryRequest::DropPath { .. } => QueryResponse::DropPath(false),
502            QueryRequest::DropAllVia { .. } => QueryResponse::DropAllVia(0),
503            QueryRequest::DropAnnounceQueues => QueryResponse::DropAnnounceQueues,
504            QueryRequest::ClearBackbonePeerState { .. } => {
505                QueryResponse::ClearBackbonePeerState(false)
506            }
507            QueryRequest::BlacklistBackbonePeer { .. } => {
508                QueryResponse::BlacklistBackbonePeer(false)
509            }
510            QueryRequest::InjectPath { .. } => QueryResponse::InjectPath(false),
511            QueryRequest::InjectIdentity { .. } => QueryResponse::InjectIdentity(false),
512            QueryRequest::RestoreKnownDestination(..) => {
513                QueryResponse::RestoreKnownDestination(false)
514            }
515            QueryRequest::RetainKnownDestination { .. } => {
516                QueryResponse::RetainKnownDestination(false)
517            }
518            QueryRequest::RetainIdentity { .. } => QueryResponse::RetainIdentity(false),
519            QueryRequest::UnretainKnownDestination { .. } => {
520                QueryResponse::UnretainKnownDestination(false)
521            }
522            QueryRequest::MarkKnownDestinationUsed { .. } => {
523                QueryResponse::MarkKnownDestinationUsed(false)
524            }
525            _ => QueryResponse::InjectIdentity(false),
526        }
527    }
528
529    /// Handle a query request and produce a response.
530    pub(crate) fn handle_query(&self, request: QueryRequest) -> QueryResponse {
531        match request {
532            QueryRequest::InterfaceStats => self.handle_interface_stats_query(),
533            QueryRequest::BackboneInterfaces => {
534                QueryResponse::BackboneInterfaces(self.list_backbone_interfaces())
535            }
536            QueryRequest::ProviderBridgeStats => {
537                #[cfg(feature = "hooks")]
538                {
539                    QueryResponse::ProviderBridgeStats(
540                        self.provider_bridge.as_ref().map(|bridge| bridge.stats()),
541                    )
542                }
543                #[cfg(not(feature = "hooks"))]
544                {
545                    QueryResponse::ProviderBridgeStats(None::<crate::event::ProviderBridgeStats>)
546                }
547            }
548            QueryRequest::DrainStatus => QueryResponse::DrainStatus(self.drain_status()),
549            QueryRequest::PathTable { max_hops } => self.handle_path_table_query(max_hops),
550            QueryRequest::RateTable => {
551                let entries: Vec<RateTableEntry> = self
552                    .engine
553                    .rate_limiter()
554                    .entries()
555                    .map(|(hash, entry)| RateTableEntry {
556                        hash: *hash,
557                        last: entry.last,
558                        rate_violations: entry.rate_violations,
559                        blocked_until: entry.blocked_until,
560                        timestamps: entry.timestamps.clone(),
561                    })
562                    .collect();
563                QueryResponse::RateTable(entries)
564            }
565            QueryRequest::LowestInterfaceBitrate => {
566                QueryResponse::LowestInterfaceBitrate(self.engine.lowest_interface_bitrate())
567            }
568            QueryRequest::MediumPathTimeout => {
569                QueryResponse::MediumPathTimeout(self.engine.medium_path_timeout())
570            }
571            QueryRequest::NextHop { dest_hash } => {
572                let resp = self
573                    .engine
574                    .next_hop(&dest_hash)
575                    .map(|next_hop| NextHopResponse {
576                        next_hop,
577                        hops: self.engine.hops_to(&dest_hash).unwrap_or(0),
578                        interface: self
579                            .engine
580                            .next_hop_interface(&dest_hash)
581                            .unwrap_or(InterfaceId(0)),
582                    });
583                QueryResponse::NextHop(resp)
584            }
585            QueryRequest::NextHopIfName { dest_hash } => {
586                let name = self
587                    .engine
588                    .next_hop_interface(&dest_hash)
589                    .and_then(|id| self.interfaces.get(&id))
590                    .map(|entry| entry.info.name.clone());
591                QueryResponse::NextHopIfName(name)
592            }
593            QueryRequest::LinkCount => QueryResponse::LinkCount(
594                self.engine.link_table_count() + self.link_manager.link_count(),
595            ),
596            QueryRequest::ActiveLinkCount => QueryResponse::ActiveLinkCount(
597                self.engine.active_link_count() + self.link_manager.active_link_count(),
598            ),
599            QueryRequest::DropPath { .. } => {
600                // Mutating queries are handled by handle_query_mut
601                QueryResponse::DropPath(false)
602            }
603            QueryRequest::DropAllVia { .. } => QueryResponse::DropAllVia(0),
604            QueryRequest::DropAnnounceQueues => QueryResponse::DropAnnounceQueues,
605            QueryRequest::TransportIdentity => {
606                QueryResponse::TransportIdentity(self.engine.identity_hash().copied())
607            }
608            QueryRequest::TransportStateSnapshot => {
609                QueryResponse::TransportStateSnapshot(self.engine.persistence_snapshot())
610            }
611            QueryRequest::GetBlackholed => {
612                let now = time::now();
613                let entries: Vec<BlackholeInfo> = self
614                    .engine
615                    .blackholed_entries()
616                    .filter(|(_, e)| e.expires == 0.0 || e.expires > now)
617                    .map(|(hash, entry)| BlackholeInfo {
618                        identity_hash: *hash,
619                        created: entry.created,
620                        expires: entry.expires,
621                        reason: entry.reason.clone(),
622                    })
623                    .collect();
624                QueryResponse::Blackholed(entries)
625            }
626            QueryRequest::BlackholeIdentity { .. } | QueryRequest::UnblackholeIdentity { .. } => {
627                // Mutating queries handled by handle_query_mut
628                QueryResponse::BlackholeResult(false)
629            }
630            QueryRequest::InjectPath { .. } => {
631                // Mutating queries handled by handle_query_mut
632                QueryResponse::InjectPath(false)
633            }
634            QueryRequest::InjectIdentity { .. } => {
635                // Mutating queries handled by handle_query_mut
636                QueryResponse::InjectIdentity(false)
637            }
638            QueryRequest::RestoreKnownDestination(..) => {
639                QueryResponse::RestoreKnownDestination(false)
640            }
641            QueryRequest::RetainKnownDestination { .. } => {
642                QueryResponse::RetainKnownDestination(false)
643            }
644            QueryRequest::RetainIdentity { .. } => QueryResponse::RetainIdentity(false),
645            QueryRequest::UnretainKnownDestination { .. } => {
646                QueryResponse::UnretainKnownDestination(false)
647            }
648            QueryRequest::MarkKnownDestinationUsed { .. } => {
649                QueryResponse::MarkKnownDestinationUsed(false)
650            }
651            QueryRequest::HasPath { dest_hash } => {
652                QueryResponse::HasPath(self.engine.has_path(&dest_hash))
653            }
654            QueryRequest::HopsTo { dest_hash } => {
655                QueryResponse::HopsTo(self.engine.hops_to(&dest_hash))
656            }
657            QueryRequest::RecallIdentity { .. } => QueryResponse::RecallIdentity(None),
658            QueryRequest::KnownDestinations => {
659                QueryResponse::KnownDestinations(self.known_destination_entries())
660            }
661            QueryRequest::LocalDestinations => {
662                let entries: Vec<LocalDestinationEntry> = self
663                    .local_destinations
664                    .iter()
665                    .map(|(hash, dest_type)| LocalDestinationEntry {
666                        hash: *hash,
667                        dest_type: *dest_type,
668                    })
669                    .collect();
670                QueryResponse::LocalDestinations(entries)
671            }
672            QueryRequest::Links => QueryResponse::Links(self.link_manager.link_entries()),
673            QueryRequest::Resources => {
674                QueryResponse::Resources(self.link_manager.resource_entries())
675            }
676            QueryRequest::DiscoveredInterfaces {
677                only_available,
678                only_transport,
679            } => {
680                let mut interfaces = self.discovered_interfaces.list().unwrap_or_default();
681                crate::discovery::filter_and_sort_interfaces(
682                    &mut interfaces,
683                    only_available,
684                    only_transport,
685                );
686                QueryResponse::DiscoveredInterfaces(interfaces)
687            }
688            request @ (QueryRequest::ListRuntimeConfig
689            | QueryRequest::GetRuntimeConfig { .. }
690            | QueryRequest::BackbonePeerState { .. }
691            | QueryRequest::SetRuntimeConfig { .. }
692            | QueryRequest::ResetRuntimeConfig { .. }
693            | QueryRequest::ClearBackbonePeerState { .. }
694            | QueryRequest::BlacklistBackbonePeer { .. }) => {
695                let fallback = Self::runtime_config_query_fallback(&request);
696                self.handle_runtime_config_query(request)
697                    .unwrap_or_else(|| {
698                        log::error!(
699                            "runtime-config query branch unexpectedly returned no response"
700                        );
701                        fallback
702                    })
703            }
704            // Mutating queries handled by handle_query_mut
705            QueryRequest::SendProbe { .. } => QueryResponse::SendProbe(None),
706            QueryRequest::CheckProof { .. } => QueryResponse::CheckProof(None),
707        }
708    }
709
710    /// Handle a mutating query request.
711    pub(crate) fn handle_query_mut(&mut self, request: QueryRequest) -> QueryResponse {
712        match request {
713            request @ (QueryRequest::BlackholeIdentity { .. }
714            | QueryRequest::UnblackholeIdentity { .. }
715            | QueryRequest::DropPath { .. }
716            | QueryRequest::DropAllVia { .. }
717            | QueryRequest::DropAnnounceQueues
718            | QueryRequest::ClearBackbonePeerState { .. }
719            | QueryRequest::BlacklistBackbonePeer { .. }
720            | QueryRequest::InjectPath { .. }
721            | QueryRequest::InjectIdentity { .. }
722            | QueryRequest::RestoreKnownDestination(..)
723            | QueryRequest::RetainKnownDestination { .. }
724            | QueryRequest::RetainIdentity { .. }
725            | QueryRequest::UnretainKnownDestination { .. }
726            | QueryRequest::MarkKnownDestinationUsed { .. }) => {
727                let fallback = Self::mutation_query_fallback(&request);
728                self.handle_mutation_query(request).unwrap_or_else(|| {
729                    log::error!("mutation query branch unexpectedly returned no response");
730                    fallback
731                })
732            }
733            QueryRequest::RecallIdentity { dest_hash } => {
734                let recalled = self.known_destination_announced(&dest_hash);
735                if recalled.is_some() {
736                    let _ = self.mark_known_destination_used(&dest_hash);
737                }
738                QueryResponse::RecallIdentity(recalled)
739            }
740            QueryRequest::DrainStatus => QueryResponse::DrainStatus(self.drain_status()),
741            QueryRequest::SendProbe {
742                dest_hash,
743                payload_size,
744            } => {
745                // Look up the identity for this destination hash
746                let announced = self.known_destination_announced(&dest_hash);
747                match announced {
748                    Some(recalled) => {
749                        let _ = self.mark_known_destination_used(&dest_hash);
750                        // Encrypt random payload with remote public key
751                        let remote_id =
752                            rns_crypto::identity::Identity::from_public_key(&recalled.public_key);
753                        let mut payload = vec![0u8; payload_size];
754                        self.rng.fill_bytes(&mut payload);
755                        match remote_id.encrypt(&payload, &mut self.rng) {
756                            Ok(ciphertext) => {
757                                // Build DATA SINGLE BROADCAST packet to dest_hash
758                                let flags = rns_core::packet::PacketFlags {
759                                    header_type: rns_core::constants::HEADER_1,
760                                    context_flag: rns_core::constants::FLAG_UNSET,
761                                    transport_type: rns_core::constants::TRANSPORT_BROADCAST,
762                                    destination_type: rns_core::constants::DESTINATION_SINGLE,
763                                    packet_type: rns_core::constants::PACKET_TYPE_DATA,
764                                };
765                                match RawPacket::pack(
766                                    flags,
767                                    0,
768                                    &dest_hash,
769                                    None,
770                                    rns_core::constants::CONTEXT_NONE,
771                                    &ciphertext,
772                                ) {
773                                    Ok(packet) => {
774                                        let packet_hash = packet.packet_hash;
775                                        let hops = self.engine.hops_to(&dest_hash).unwrap_or(0);
776                                        // Track for proof matching
777                                        self.sent_packets
778                                            .insert(packet_hash, (dest_hash, time::now()));
779                                        // Send via engine
780                                        let actions = self.engine.handle_outbound(
781                                            &packet,
782                                            rns_core::constants::DESTINATION_SINGLE,
783                                            None,
784                                            time::now(),
785                                        );
786                                        self.dispatch_all(actions);
787                                        log::debug!(
788                                            "Sent probe ({} bytes) to {:02x?}",
789                                            payload_size,
790                                            &dest_hash[..4],
791                                        );
792                                        QueryResponse::SendProbe(Some((packet_hash, hops)))
793                                    }
794                                    Err(_) => {
795                                        log::warn!("Failed to pack probe packet");
796                                        QueryResponse::SendProbe(None)
797                                    }
798                                }
799                            }
800                            Err(_) => {
801                                log::warn!("Failed to encrypt probe payload");
802                                QueryResponse::SendProbe(None)
803                            }
804                        }
805                    }
806                    None => {
807                        log::debug!("No known identity for probe dest {:02x?}", &dest_hash[..4]);
808                        QueryResponse::SendProbe(None)
809                    }
810                }
811            }
812            QueryRequest::CheckProof { packet_hash } => {
813                match self.completed_proofs.remove(&packet_hash) {
814                    Some((rtt, _received)) => QueryResponse::CheckProof(Some(rtt)),
815                    None => QueryResponse::CheckProof(None),
816                }
817            }
818            QueryRequest::SetRuntimeConfig { key, value } => {
819                let result = match key.as_str() {
820                    "global.tick_interval_ms" => match Self::expect_u64(value, &key) {
821                        Ok(value) => {
822                            let clamped = value.clamp(100, 10_000);
823                            self.tick_interval_ms.store(clamped, Ordering::Relaxed);
824                            Ok(())
825                        }
826                        Err(err) => Err(err),
827                    },
828                    "global.known_destinations_ttl_secs" => match Self::expect_f64(value, &key) {
829                        Ok(value) => {
830                            self.known_destinations_ttl = value;
831                            Ok(())
832                        }
833                        Err(err) => Err(err),
834                    },
835                    "global.rate_limiter_ttl_secs" => match Self::expect_f64(value, &key) {
836                        Ok(value) if value >= 0.0 => {
837                            self.rate_limiter_ttl_secs = value;
838                            Ok(())
839                        }
840                        Ok(_) => Err(RuntimeConfigError {
841                            code: RuntimeConfigErrorCode::InvalidValue,
842                            message: format!("{} must be >= 0", key),
843                        }),
844                        Err(err) => Err(err),
845                    },
846                    "global.known_destinations_cleanup_interval_ticks" => {
847                        match Self::expect_u64(value, &key) {
848                            Ok(value) if value > 0 => {
849                                self.known_destinations_cleanup_interval_ticks = value as u32;
850                                Ok(())
851                            }
852                            Ok(_) => Err(RuntimeConfigError {
853                                code: RuntimeConfigErrorCode::InvalidValue,
854                                message: format!("{} must be >= 1", key),
855                            }),
856                            Err(err) => Err(err),
857                        }
858                    }
859                    "global.announce_cache_cleanup_interval_ticks" => {
860                        match Self::expect_u64(value, &key) {
861                            Ok(value) if value > 0 => {
862                                self.announce_cache_cleanup_interval_ticks = value as u32;
863                                Ok(())
864                            }
865                            Ok(_) => Err(RuntimeConfigError {
866                                code: RuntimeConfigErrorCode::InvalidValue,
867                                message: format!("{} must be >= 1", key),
868                            }),
869                            Err(err) => Err(err),
870                        }
871                    }
872                    "global.announce_cache_cleanup_batch_size" => {
873                        match Self::expect_u64(value, &key) {
874                            Ok(value) if value > 0 => {
875                                self.announce_cache_cleanup_batch_size = value as usize;
876                                Ok(())
877                            }
878                            Ok(_) => Err(RuntimeConfigError {
879                                code: RuntimeConfigErrorCode::InvalidValue,
880                                message: format!("{} must be >= 1", key),
881                            }),
882                            Err(err) => Err(err),
883                        }
884                    }
885                    "global.discovery_cleanup_interval_ticks" => {
886                        match Self::expect_u64(value, &key) {
887                            Ok(value) if value > 0 => {
888                                self.discovery_cleanup_interval_ticks = value as u32;
889                                Ok(())
890                            }
891                            Ok(_) => Err(RuntimeConfigError {
892                                code: RuntimeConfigErrorCode::InvalidValue,
893                                message: format!("{} must be >= 1", key),
894                            }),
895                            Err(err) => Err(err),
896                        }
897                    }
898                    "global.management_announce_interval_secs" => {
899                        match Self::expect_f64(value, &key) {
900                            Ok(value) => {
901                                self.management_announce_interval_secs = value;
902                                Ok(())
903                            }
904                            Err(err) => Err(err),
905                        }
906                    }
907                    "global.direct_connect_policy" => {
908                        let policy = match Self::parse_holepunch_policy(&value) {
909                            Some(policy) => policy,
910                            None => {
911                                return QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
912                                    code: RuntimeConfigErrorCode::InvalidValue,
913                                    message: format!(
914                                        "{} must be one of: reject, accept_all, identified_only, ask_app",
915                                        key
916                                    ),
917                                }))
918                            }
919                        };
920                        self.holepunch_manager.set_policy(policy);
921                        Ok(())
922                    }
923                    #[cfg(feature = "hooks")]
924                    "provider.queue_max_events" => match Self::expect_u64(value, &key) {
925                        Ok(v) if v > 0 => {
926                            if let Some(ref bridge) = self.provider_bridge {
927                                bridge.set_queue_max_events(v as usize);
928                            }
929                            Ok(())
930                        }
931                        Ok(_) => Err(RuntimeConfigError {
932                            code: RuntimeConfigErrorCode::InvalidValue,
933                            message: format!("{} must be >= 1", key),
934                        }),
935                        Err(err) => Err(err),
936                    },
937                    #[cfg(feature = "hooks")]
938                    "provider.queue_max_bytes" => match Self::expect_u64(value, &key) {
939                        Ok(v) if v > 0 => {
940                            if let Some(ref bridge) = self.provider_bridge {
941                                bridge.set_queue_max_bytes(v as usize);
942                            }
943                            Ok(())
944                        }
945                        Ok(_) => Err(RuntimeConfigError {
946                            code: RuntimeConfigErrorCode::InvalidValue,
947                            message: format!("{} must be >= 1", key),
948                        }),
949                        Err(err) => Err(err),
950                    },
951                    _ => match Self::runtime_config_family_for_key(&key) {
952                        Some(family) => self.set_runtime_config_family_value(family, &key, value),
953                        None => {
954                            return QueryResponse::RuntimeConfigSet(Err(RuntimeConfigError {
955                                code: RuntimeConfigErrorCode::UnknownKey,
956                                message: format!("unknown runtime-config key '{}'", key),
957                            }))
958                        }
959                    },
960                };
961
962                QueryResponse::RuntimeConfigSet(match result {
963                    Ok(()) => self.runtime_config_entry(&key).ok_or(RuntimeConfigError {
964                        code: RuntimeConfigErrorCode::ApplyFailed,
965                        message: format!("failed to read back runtime-config key '{}'", key),
966                    }),
967                    Err(err) => Err(err),
968                })
969            }
970            QueryRequest::ResetRuntimeConfig { key } => {
971                let defaults = self.runtime_config_defaults;
972                let result = match key.as_str() {
973                    "global.tick_interval_ms" => {
974                        self.tick_interval_ms
975                            .store(defaults.tick_interval_ms, Ordering::Relaxed);
976                        Ok(())
977                    }
978                    "global.known_destinations_ttl_secs" => {
979                        self.known_destinations_ttl = defaults.known_destinations_ttl;
980                        Ok(())
981                    }
982                    "global.rate_limiter_ttl_secs" => {
983                        self.rate_limiter_ttl_secs = defaults.rate_limiter_ttl_secs;
984                        Ok(())
985                    }
986                    "global.known_destinations_cleanup_interval_ticks" => {
987                        self.known_destinations_cleanup_interval_ticks =
988                            defaults.known_destinations_cleanup_interval_ticks;
989                        Ok(())
990                    }
991                    "global.announce_cache_cleanup_interval_ticks" => {
992                        self.announce_cache_cleanup_interval_ticks =
993                            defaults.announce_cache_cleanup_interval_ticks;
994                        Ok(())
995                    }
996                    "global.announce_cache_cleanup_batch_size" => {
997                        self.announce_cache_cleanup_batch_size =
998                            defaults.announce_cache_cleanup_batch_size;
999                        Ok(())
1000                    }
1001                    "global.discovery_cleanup_interval_ticks" => {
1002                        self.discovery_cleanup_interval_ticks =
1003                            defaults.discovery_cleanup_interval_ticks;
1004                        Ok(())
1005                    }
1006                    "global.management_announce_interval_secs" => {
1007                        self.management_announce_interval_secs =
1008                            defaults.management_announce_interval_secs;
1009                        Ok(())
1010                    }
1011                    "global.direct_connect_policy" => {
1012                        self.holepunch_manager
1013                            .set_policy(defaults.direct_connect_policy);
1014                        Ok(())
1015                    }
1016                    #[cfg(feature = "hooks")]
1017                    "provider.queue_max_events" => {
1018                        if let Some(ref bridge) = self.provider_bridge {
1019                            bridge.set_queue_max_events(defaults.provider_queue_max_events);
1020                        }
1021                        Ok(())
1022                    }
1023                    #[cfg(feature = "hooks")]
1024                    "provider.queue_max_bytes" => {
1025                        if let Some(ref bridge) = self.provider_bridge {
1026                            bridge.set_queue_max_bytes(defaults.provider_queue_max_bytes);
1027                        }
1028                        Ok(())
1029                    }
1030                    _ => match Self::runtime_config_family_for_key(&key) {
1031                        Some(family) => self.reset_runtime_config_family_value(family, &key),
1032                        None => {
1033                            return QueryResponse::RuntimeConfigReset(Err(RuntimeConfigError {
1034                                code: RuntimeConfigErrorCode::UnknownKey,
1035                                message: format!("unknown runtime-config key '{}'", key),
1036                            }))
1037                        }
1038                    },
1039                };
1040
1041                QueryResponse::RuntimeConfigReset(match result {
1042                    Ok(()) => self.runtime_config_entry(&key).ok_or(RuntimeConfigError {
1043                        code: RuntimeConfigErrorCode::ApplyFailed,
1044                        message: format!("failed to read back runtime-config key '{}'", key),
1045                    }),
1046                    Err(err) => Err(err),
1047                })
1048            }
1049            other => self.handle_query(other),
1050        }
1051    }
1052}