Skip to main content

iota_types/
traffic_control.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::path::PathBuf;
6
7use serde::{Deserialize, Serialize, de::Deserializer};
8use serde_with::serde_as;
9
10// These values set to loosely attempt to limit
11// memory usage for a single sketch to ~20MB
12// For reference, see
13// https://github.com/jedisct1/rust-count-min-sketch/blob/master/src/lib.rs
14pub const DEFAULT_SKETCH_CAPACITY: usize = 50_000;
15pub const DEFAULT_SKETCH_PROBABILITY: f64 = 0.999;
16pub const DEFAULT_SKETCH_TOLERANCE: f64 = 0.2;
17use rand::distributions::Distribution;
18
19const TRAFFIC_SINK_TIMEOUT_SEC: u64 = 300;
20
21/// The source that should be used to identify the client's
22/// IP address. To be used to configure cases where a node has
23/// infra running in front of the node that is separate from the
24/// protocol, such as a load balancer. Note that this is not the
25/// same as the client type (e.g a direct client vs a proxy client,
26/// as in the case of a fullnode driving requests from many clients).
27///
28/// For x-forwarded-for, the usize parameter is the number of forwarding
29/// hops between the client and the node for requests going your infra
30/// or infra provider. Example:
31///
32/// ```ignore
33///     (client) -> { (global proxy) -> (regional proxy) -> (node) }
34/// ```
35///
36/// where
37///
38/// ```ignore
39///     { <server>, ... }
40/// ```
41///
42/// are controlled by the Node operator / their cloud provider.
43/// In this case, we set:
44///
45/// ```ignore
46/// policy-config:
47///    client-id-source:
48///      x-forwarded-for: 2
49///    ...
50/// ```
51///
52/// NOTE: x-forwarded-for: 0 is a special case value that can be used by Node
53/// operators to discover the number of hops that should be configured. To use:
54///
55/// 1. Set `x-forwarded-for: 0` for the `client-id-source` in the config.
56/// 2. Run the node and query any endpoint (AuthorityServer for validator, or
57///    json rpc for rpc node) from a known IP address.
58/// 3. Search for lines containing `x-forwarded-for` in the logs. The log lines
59///    should contain the contents of the `x-forwarded-for` header, if present,
60///    or a corresponding error if not.
61/// 4. The value for number of hops is derived from any such log line that
62///    contains your known IP address, and is defined as 1 + the number of IP
63///    addresses in the `x-forwarded-for` that occur **after** the known client
64///    IP address. Example:
65///
66/// ```ignore
67///     [<known client IP>] <--- number of hops is 1
68///     ["1.2.3.4", <known client IP>, "5.6.7.8", "9.10.11.12"] <--- number of hops is 3
69/// ```
70#[derive(Clone, Debug, Deserialize, Serialize, Default)]
71#[serde(rename_all = "kebab-case")]
72pub enum ClientIdSource {
73    #[default]
74    SocketAddr,
75    XForwardedFor(usize),
76}
77
78#[derive(Clone, Debug, Deserialize, Serialize)]
79pub struct TrafficControlReconfigParams {
80    pub error_threshold: Option<u64>,
81    pub spam_threshold: Option<u64>,
82    pub dry_run: Option<bool>,
83}
84
85#[derive(Clone, Debug, Deserialize, Serialize)]
86pub struct Weight(f32);
87
88impl Weight {
89    pub fn new(value: f32) -> Result<Self, &'static str> {
90        if (0.0..=1.0).contains(&value) {
91            Ok(Self(value))
92        } else {
93            Err("Weight must be between 0.0 and 1.0")
94        }
95    }
96
97    pub fn one() -> Self {
98        Self(1.0)
99    }
100
101    pub fn zero() -> Self {
102        Self(0.0)
103    }
104
105    pub fn value(&self) -> f32 {
106        self.0
107    }
108
109    pub fn is_sampled(&self) -> bool {
110        let mut rng = rand::thread_rng();
111        // `Uniform::new` excludes the upper bound, so a weight of 1.0 accepts every
112        // sample.
113        let sample = rand::distributions::Uniform::new(0.0, 1.0).sample(&mut rng);
114        self.accepts(sample)
115    }
116
117    fn accepts(&self, sample: f32) -> bool {
118        sample < self.value()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::Weight;
125
126    #[test]
127    fn zero_weight_rejects_the_lowest_sample() {
128        assert!(!Weight::zero().accepts(0.0));
129    }
130
131    #[test]
132    fn full_weight_accepts_the_highest_sample() {
133        assert!(Weight::one().accepts(1.0 - f32::EPSILON));
134    }
135}
136
137fn validate_sample_rate<'de, D>(deserializer: D) -> Result<Weight, D::Error>
138where
139    D: Deserializer<'de>,
140{
141    let value = f32::deserialize(deserializer)?;
142    Weight::new(value)
143        .map_err(|_| serde::de::Error::custom("spam-sample-rate must be between 0.0 and 1.0"))
144}
145
146impl PartialEq for Weight {
147    fn eq(&self, other: &Self) -> bool {
148        self.value() == other.value()
149    }
150}
151
152#[serde_as]
153#[derive(Clone, Debug, Deserialize, Serialize)]
154#[serde(rename_all = "kebab-case")]
155pub struct RemoteFirewallConfig {
156    pub remote_fw_url: String,
157    pub destination_port: u16,
158    #[serde(default)]
159    pub delegate_spam_blocking: bool,
160    #[serde(default)]
161    pub delegate_error_blocking: bool,
162    #[serde(default = "default_drain_path")]
163    pub drain_path: PathBuf,
164    /// Time in secs, after which no registered ingress traffic
165    /// will trigger dead mans switch to drain any firewalls
166    #[serde(default = "default_drain_timeout")]
167    pub drain_timeout_secs: u64,
168}
169
170fn default_drain_path() -> PathBuf {
171    PathBuf::from("/tmp/drain")
172}
173
174fn default_drain_timeout() -> u64 {
175    TRAFFIC_SINK_TIMEOUT_SEC
176}
177
178#[serde_as]
179#[derive(Clone, Debug, Deserialize, Serialize)]
180#[serde(rename_all = "kebab-case")]
181pub struct FreqThresholdConfig {
182    #[serde(default = "default_client_threshold")]
183    pub client_threshold: u64,
184    #[serde(default = "default_proxied_client_threshold")]
185    pub proxied_client_threshold: u64,
186    #[serde(default = "default_window_size_secs")]
187    pub window_size_secs: u64,
188    #[serde(default = "default_update_interval_secs")]
189    pub update_interval_secs: u64,
190    #[serde(default = "default_sketch_capacity")]
191    pub sketch_capacity: usize,
192    #[serde(default = "default_sketch_probability")]
193    pub sketch_probability: f64,
194    #[serde(default = "default_sketch_tolerance")]
195    pub sketch_tolerance: f64,
196}
197
198impl Default for FreqThresholdConfig {
199    fn default() -> Self {
200        Self {
201            client_threshold: default_client_threshold(),
202            proxied_client_threshold: default_proxied_client_threshold(),
203            window_size_secs: default_window_size_secs(),
204            update_interval_secs: default_update_interval_secs(),
205            sketch_capacity: default_sketch_capacity(),
206            sketch_probability: default_sketch_probability(),
207            sketch_tolerance: default_sketch_tolerance(),
208        }
209    }
210}
211
212fn default_client_threshold() -> u64 {
213    // by default only block client with unreasonably
214    // high qps, as a client could be a single fullnode proxying
215    // the majority of traffic from many behaving clients in normal
216    // operations. If used as a spam policy, all requests would
217    // count against this threshold within the window time. In
218    // practice this should always be set
219    1_000_000
220}
221
222fn default_proxied_client_threshold() -> u64 {
223    10
224}
225
226fn default_window_size_secs() -> u64 {
227    30
228}
229
230fn default_update_interval_secs() -> u64 {
231    5
232}
233
234fn default_sketch_capacity() -> usize {
235    DEFAULT_SKETCH_CAPACITY
236}
237
238fn default_sketch_probability() -> f64 {
239    DEFAULT_SKETCH_PROBABILITY
240}
241
242fn default_sketch_tolerance() -> f64 {
243    DEFAULT_SKETCH_TOLERANCE
244}
245
246// Serializable representation of policy types, used in config
247// in order to easily change in tests or to killswitch
248#[derive(Clone, Serialize, Deserialize, Debug, Default)]
249pub enum PolicyType {
250    /// Does nothing
251    #[default]
252    NoOp,
253
254    /// Blocks connection_ip after reaching a tally frequency (tallies per
255    /// second) of `threshold`, as calculated over an average window of
256    /// `window_size_secs` with granularity of `update_interval_secs`
257    #[serde(rename = "freq-threshold", alias = "FreqThreshold")]
258    FreqThreshold(FreqThresholdConfig),
259
260    // Below this point are test policies, and thus should not be used in production
261    /// Simple policy that adds connection_ip to blocklist when the same
262    /// connection_ip is encountered in tally N times. If used in an error
263    /// policy, this would trigger after N errors
264    TestNConnIP(u64),
265    /// Test policy that panics when invoked. To be used as an error policy in
266    /// tests that do not expect request errors in order to verify that the
267    /// error policy is not invoked
268    TestPanicOnInvocation,
269}
270
271#[serde_as]
272#[derive(Clone, Debug, Deserialize, Serialize)]
273#[serde(rename_all = "kebab-case")]
274pub struct PolicyConfig {
275    #[serde(default = "default_client_id_source")]
276    pub client_id_source: ClientIdSource,
277    #[serde(default = "default_connection_blocklist_ttl_sec")]
278    pub connection_blocklist_ttl_sec: u64,
279    #[serde(default)]
280    pub proxy_blocklist_ttl_sec: u64,
281    #[serde(default)]
282    pub spam_policy_type: PolicyType,
283    #[serde(default)]
284    pub error_policy_type: PolicyType,
285    #[serde(default = "default_channel_capacity")]
286    pub channel_capacity: usize,
287    #[serde(
288        default = "default_spam_sample_rate",
289        deserialize_with = "validate_sample_rate"
290    )]
291    /// Note that this sample policy is applied on top of the
292    /// endpoint-specific sample policy (not configurable) which
293    /// weighs endpoints by the relative effort required to serve
294    /// them. Therefore a sample rate of N will yield an actual
295    /// sample rate <= N.
296    pub spam_sample_rate: Weight,
297    #[serde(default = "default_dry_run")]
298    pub dry_run: bool,
299    /// List of String which should all parse to type IPAddr.
300    /// If set, only requests from provided IPs will be allowed,
301    /// and any blocklist related configuration will be ignored.
302    #[serde(default)]
303    pub allow_list: Option<Vec<String>>,
304}
305
306impl Default for PolicyConfig {
307    fn default() -> Self {
308        Self {
309            client_id_source: default_client_id_source(),
310            connection_blocklist_ttl_sec: 0,
311            proxy_blocklist_ttl_sec: 0,
312            spam_policy_type: PolicyType::NoOp,
313            error_policy_type: PolicyType::NoOp,
314            channel_capacity: 100,
315            spam_sample_rate: default_spam_sample_rate(),
316            dry_run: default_dry_run(),
317            allow_list: None,
318        }
319    }
320}
321
322impl PolicyConfig {
323    pub fn default_dos_protection_policy() -> Self {
324        Self {
325            client_id_source: ClientIdSource::SocketAddr,
326            spam_policy_type: PolicyType::FreqThreshold(FreqThresholdConfig {
327                client_threshold: 1000,
328                window_size_secs: 5,
329                update_interval_secs: 1,
330                ..FreqThresholdConfig::default()
331            }),
332            error_policy_type: PolicyType::FreqThreshold(FreqThresholdConfig {
333                client_threshold: 50,
334                window_size_secs: 5,
335                update_interval_secs: 1,
336                ..FreqThresholdConfig::default()
337            }),
338            channel_capacity: 6000,
339            spam_sample_rate: Weight::new(1.0).unwrap(),
340            dry_run: true,
341            ..Self::default()
342        }
343    }
344}
345
346pub fn default_client_id_source() -> ClientIdSource {
347    ClientIdSource::SocketAddr
348}
349
350pub fn default_connection_blocklist_ttl_sec() -> u64 {
351    60
352}
353pub fn default_channel_capacity() -> usize {
354    100
355}
356
357pub fn default_dry_run() -> bool {
358    true
359}
360
361pub fn default_spam_sample_rate() -> Weight {
362    Weight::new(0.2).unwrap()
363}