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;