Skip to main content

decdn_node/dht/
rate_limit.rs

1//! Three-layer token-bucket rate limiter for `cdn/dht/v1` inbound traffic
2//! (ADR 022 §DHT Rate Limiting).
3//!
4//! This is a thin DHT-flavoured wrapper over the shared
5//! [`crate::rate_limit::ThreeLayerRateLimiter`] engine: it pins the
6//! DHT-specific operator-visible metric names (via a private metrics sink)
7//! and re-exports the config/reject types under their historical DHT names.
8//! The layering, cheapest-first ordering, keyspace bound, and batch-token
9//! accounting all live in the shared engine — see its
10//! module docs.
11
12use std::sync::Arc;
13
14use crate::metrics::Metrics;
15use crate::rate_limit::{
16    RateLimitConfig, RateLimitMetricsSink, RejectLayer, ThreeLayerRateLimiter,
17};
18
19#[cfg(test)]
20use std::net::IpAddr;
21#[cfg(test)]
22use std::time::Duration;
23
24#[cfg(test)]
25use crate::dht::routing::NodeId;
26
27/// Resolved DHT rate-limit configuration. See ADR 022 §DHT Rate Limiting.
28/// [`Default`] returns the ADR 022 values (per-peer 20/40, per-IP 100/200,
29/// global 1000/2000).
30pub type DhtRateLimitConfig = RateLimitConfig;
31
32/// Layer that triggered a `cdn/dht/v1` rejection. Alias of the shared
33/// [`RejectLayer`]; the `per_peer`/`per_ip`/`global` label strings are stable.
34pub type DhtRejectLayer = RejectLayer;
35
36/// Routes shared-limiter metric events to the DHT operator-visible counters
37/// (`decdn_dht_rate_limit_*`): one unlabeled Counter per layer, per the
38/// sibling-counter convention recorded in
39/// `adr/appendix-observability.md` § Reason splits. Operators recover the
40/// rolled-up rate with
41/// `sum(rate({__name__=~"decdn_dht_rate_limit_rejected_(per_peer|per_ip|global)_total"}[1m]))`.
42///
43/// **This is a choice, not a backend limitation** — `iroh_metrics` supports
44/// per-field labels: `DecdnMetrics::probe_hold_unavailable` and
45/// `DecdnMetrics::streams_active` are both `Family<L, M>` (private fields; see
46/// [`crate::metrics::Metrics::probe_hold_unavailable`] for the accessor that
47/// uses one). Each layer here has an unrelated remedy (one abusive
48/// peer, one abusive host, aggregate load), so no alert spans the family and a
49/// shared label would buy nothing. The appendix registry carries this trio of
50/// unlabelled per-layer counters rather than a single labelled
51/// `decdn_dht_rate_limit_rejections_total`.
52struct DhtRateLimitMetrics(Arc<Metrics>);
53
54impl RateLimitMetricsSink for DhtRateLimitMetrics {
55    fn rejected(&self, layer: RejectLayer) {
56        match layer {
57            RejectLayer::Global => self.0.dht_rate_limit_rejected_global(),
58            RejectLayer::PerIp => self.0.dht_rate_limit_rejected_per_ip(),
59            RejectLayer::PerPeer => self.0.dht_rate_limit_rejected_per_peer(),
60        }
61    }
62
63    fn prune_sweep_per_ip(&self) {
64        self.0.dht_rate_limit_prune_sweep_per_ip();
65    }
66
67    fn prune_sweep_per_peer(&self) {
68        self.0.dht_rate_limit_prune_sweep_per_peer();
69    }
70
71    fn set_tracked_per_ip(&self, n: usize) {
72        self.0.dht_rate_limit_tracked_per_ip_set(n);
73    }
74
75    fn set_tracked_per_peer(&self, n: usize) {
76        self.0.dht_rate_limit_tracked_per_peer_set(n);
77    }
78}
79
80/// Three-layer rate limiter for inbound `cdn/dht/v1` requests. Newtype over the
81/// shared [`ThreeLayerRateLimiter`] that injects the DHT metrics sink.
82#[allow(missing_debug_implementations)]
83pub struct DhtRateLimiter(ThreeLayerRateLimiter);
84
85impl DhtRateLimiter {
86    /// Build a limiter from the resolved DHT config snapshot.
87    #[must_use]
88    pub fn new(cfg: &DhtRateLimitConfig, metrics: Arc<Metrics>) -> Self {
89        Self(ThreeLayerRateLimiter::new(
90            cfg,
91            Arc::new(DhtRateLimitMetrics(metrics)),
92        ))
93    }
94
95    /// Build a limiter straight from the resolved `[dht.rate_limit]` section.
96    ///
97    /// Prefer this over [`Self::new`] at wiring sites: `DhtRateLimitConfig` is
98    /// an alias of the shared [`RateLimitConfig`], so `new` cannot tell a config
99    /// mapped from `[dht.rate_limit]` from one mapped out of
100    /// `[probe.rate_limit]`. Taking `&ResolvedDht` makes the wrong pairing a
101    /// type error (#1457); see
102    /// [`crate::handlers::probe_rate_limit::ProbeRateLimiter::from_resolved`]
103    /// for the direction that actually loosens a cap.
104    #[must_use]
105    pub fn from_resolved(
106        resolved: &decdn_common::config::ResolvedDht,
107        metrics: Arc<Metrics>,
108    ) -> Self {
109        Self::new(&RateLimitConfig::from(resolved), metrics)
110    }
111
112    /// Try to admit one inbound DHT request. See
113    /// [`ThreeLayerRateLimiter::check`].
114    pub fn check(
115        &self,
116        peer_node_id: &crate::dht::routing::NodeId,
117        peer_ip: Option<std::net::IpAddr>,
118    ) -> Result<(), DhtRejectLayer> {
119        self.0.check(peer_node_id, peer_ip)
120    }
121
122    /// Stage 2 of batch admission. See
123    /// [`ThreeLayerRateLimiter::admit_batch_extra`].
124    #[must_use]
125    pub fn admit_batch_extra(
126        &self,
127        peer_node_id: &crate::dht::routing::NodeId,
128        peer_ip: Option<std::net::IpAddr>,
129        extra: usize,
130    ) -> usize {
131        self.0.admit_batch_extra(peer_node_id, peer_ip, extra)
132    }
133
134    /// Periodic GC sweep for the per-IP keyed map (#645).
135    #[must_use]
136    pub fn gc_per_ip(&self) -> Option<(usize, usize)> {
137        self.0.gc_per_ip()
138    }
139
140    /// Periodic GC sweep for the per-peer keyed map (#645).
141    #[must_use]
142    pub fn gc_per_peer(&self) -> Option<(usize, usize)> {
143        self.0.gc_per_peer()
144    }
145
146    /// Current tracked-key count for the per-IP keyed map (#645).
147    #[must_use]
148    pub fn per_ip_tracked(&self) -> usize {
149        self.0.per_ip_tracked()
150    }
151
152    /// Current tracked-key count for the per-peer keyed map (#645).
153    #[must_use]
154    pub fn per_peer_tracked(&self) -> usize {
155        self.0.per_peer_tracked()
156    }
157}
158
159#[cfg(test)]
160#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
161mod tests;