Skip to content

Commit 9eb9848

Browse files
committed
feat: [torrust#1096] use exact IP banning list
We are using a Counting Bloom Filter to count IPs sending wrong connections IDs. IPs are banned after sending 10 wrong connections IDs. CBFs are fast and use litle memory but they are also innaccurate. They have False Positives meaning some IPs would be banned only becuase there are bucket colissions (IPs sharing the same counter). To avoid banning IPs incorrectly we decided to introduce a second counter, which is a HashMap counting error is a exact way. IPs are only banned when this counter reaches the limit. We keep the CBF as a first level filter. It's a fast check to filter IPs without affecting tracker's performance. When the IP is banned according tho the first filter we start a counter for that IP in the second exact counter. This solution should be good if the number of IPs is low. We have to find another solution anyway for IPv6 where is cheaper to own a range if IPs.
1 parent ce3b19e commit 9eb9848

File tree

1 file changed

+45
-17
lines changed

1 file changed

+45
-17
lines changed

src/servers/udp/server/banning.rs

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22
//!
33
//! It bans clients that send invalid connection id's.
44
//!
5-
//! It uses a Counting Bloom Filter to keep track of the number of connection id
6-
//! errors per ip. That means there can be false positives, but not false
7-
//! negatives.
5+
//! It uses two levels of filtering:
86
//!
9-
//! 1 out of 100000 requests will be a false positive and the client will not
10-
//! receive a response.
7+
//! 1. First, tt uses a Counting Bloom Filter to keep track of the number of
8+
//! connection ID errors per ip. That means there can be false positives, but
9+
//! not false negatives. 1 out of 100000 requests will be a false positive
10+
//! and the client will be banned and not receive a response.
11+
//! 2. Since we want to avoid false positives (banning a client that is not
12+
//! sending invalid connection id's), we use a `HashMap` to keep track of the
13+
//! exact number of connection ID errors per ip.
14+
//!
15+
//! This two level filtering is to avoid false positives. It has the advantage
16+
//! of being fast by using a Counting Bloom Filter and not having false
17+
//! negatives at the cost of increasing the memory usage.
18+
use std::collections::HashMap;
1119
use std::net::IpAddr;
1220

1321
use bloom::{CountingBloomFilter, ASMS};
@@ -18,7 +26,8 @@ use crate::servers::udp::UDP_TRACKER_LOG_TARGET;
1826

1927
pub struct BanService {
2028
max_connection_id_errors_per_ip: u32,
21-
cbf: CountingBloomFilter,
29+
fuzzy_error_counter: CountingBloomFilter,
30+
accurate_error_counter: HashMap<IpAddr, u32>,
2231
local_addr: Url,
2332
last_connection_id_errors_reset: Instant,
2433
}
@@ -29,30 +38,47 @@ impl BanService {
2938
Self {
3039
max_connection_id_errors_per_ip,
3140
local_addr,
32-
cbf: CountingBloomFilter::with_rate(4, 0.01, 100),
41+
fuzzy_error_counter: CountingBloomFilter::with_rate(4, 0.01, 100),
42+
accurate_error_counter: HashMap::new(),
3343
last_connection_id_errors_reset: tokio::time::Instant::now(),
3444
}
3545
}
3646

3747
pub fn increase_counter(&mut self, ip: &IpAddr) {
38-
self.cbf.insert(&ip.to_string());
48+
self.fuzzy_error_counter.insert(&ip.to_string());
49+
*self.accurate_error_counter.entry(*ip).or_insert(0) += 1;
3950
}
4051

41-
pub fn get_counter(&mut self, ip: &IpAddr) -> u32 {
42-
self.cbf.estimate_count(&ip.to_string())
52+
#[must_use]
53+
pub fn get_count(&self, ip: &IpAddr) -> Option<u32> {
54+
self.accurate_error_counter.get(ip).copied()
55+
}
56+
57+
#[must_use]
58+
pub fn get_estimate_count(&self, ip: &IpAddr) -> u32 {
59+
self.fuzzy_error_counter.estimate_count(&ip.to_string())
4360
}
4461

4562
/// Returns true if the given ip address is banned.
4663
#[must_use]
4764
pub fn is_banned(&self, ip: &IpAddr) -> bool {
48-
let connection_id_errors_from_ip = self.cbf.estimate_count(&ip.to_string());
65+
// First check if the ip is in the bloom filter (fast check)
66+
if self.fuzzy_error_counter.estimate_count(&ip.to_string()) <= self.max_connection_id_errors_per_ip {
67+
return false;
68+
}
4969

50-
connection_id_errors_from_ip > self.max_connection_id_errors_per_ip
70+
// Check with the exact counter (to avoid false positives)
71+
match self.get_count(ip) {
72+
Some(count) => count > self.max_connection_id_errors_per_ip,
73+
None => false,
74+
}
5175
}
5276

53-
/// Resets the filter and updates the reset timestamp.
77+
/// Resets the filters and updates the reset timestamp.
5478
pub fn reset_bans(&mut self) {
55-
self.cbf.clear();
79+
self.fuzzy_error_counter.clear();
80+
81+
self.accurate_error_counter.clear();
5682

5783
self.last_connection_id_errors_reset = Instant::now();
5884

@@ -74,14 +100,14 @@ mod tests {
74100
}
75101

76102
#[test]
77-
fn it_should_increase_the_ip_counter() {
103+
fn it_should_increase_the_errors_counter_for_a_given_ip() {
78104
let mut ban_service = ban_service(1);
79105

80106
let ip: IpAddr = "127.0.0.2".parse().unwrap();
81107

82108
ban_service.increase_counter(&ip);
83109

84-
assert_eq!(ban_service.get_counter(&ip), 1);
110+
assert_eq!(ban_service.get_count(&ip), Some(1));
85111
}
86112

87113
#[test]
@@ -93,6 +119,8 @@ mod tests {
93119
ban_service.increase_counter(&ip); // Counter = 1
94120
ban_service.increase_counter(&ip); // Counter = 2
95121

122+
println!("Counter: {}", ban_service.get_count(&ip).unwrap());
123+
96124
assert!(ban_service.is_banned(&ip));
97125
}
98126

@@ -117,6 +145,6 @@ mod tests {
117145

118146
ban_service.reset_bans();
119147

120-
assert_eq!(ban_service.get_counter(&ip), 0);
148+
assert_eq!(ban_service.get_estimate_count(&ip), 0);
121149
}
122150
}

0 commit comments

Comments
 (0)