hashiverse_server_lib/transport/ddos/
ipset_ddos.rs1use hashiverse_lib::transport::ddos::ddos::{DdosProtection, DdosScore};
24use parking_lot::Mutex;
25use log::{info, warn};
26use moka::sync::Cache;
27use std::collections::HashMap;
28use std::process::Command;
29use std::sync::Arc;
30use std::time::Duration;
31
32pub struct IpsetDdosProtection {
46 set_name: String,
47 score_threshold: f64,
48 decay_per_second: f64,
49 bad_request_penalty: f64,
50 max_connections_per_ip: usize,
51 scores: Cache<String, Arc<Mutex<DdosScore>>>,
52 ipset_throttle: Cache<String, ()>,
53 connections: Mutex<HashMap<String, usize>>,
54}
55
56impl IpsetDdosProtection {
57 pub fn new(set_name: impl Into<String>, score_threshold: f64, decay_per_second: f64, bad_request_penalty: f64, max_connections_per_ip: usize) -> Self {
58 let set_name = set_name.into();
59
60 let idle_secs = if decay_per_second > 0.0 {
62 (score_threshold / decay_per_second * 2.0).ceil() as u64
63 } else {
64 3600
65 };
66
67 let result = Command::new("ipset").args(["create", &set_name, "hash:ip", "timeout", &idle_secs.to_string(), "--exist"]).status();
68 match result {
69 Ok(status) if status.success() => info!("DDoS: ipset set '{}' ready", set_name),
70 Ok(status) => warn!("DDoS: ipset create '{}' failed with status {}", set_name, status),
71 Err(e) => warn!("DDoS: failed to run ipset create '{}': {}", set_name, e),
72 }
73
74 for chain in ["INPUT", "FORWARD"] {
76 let _ = Command::new("iptables").args(["-D", chain, "-m", "set", "--match-set", &set_name, "src", "-j", "DROP"]).status();
78 let result = Command::new("iptables").args(["-I", chain, "-m", "set", "--match-set", &set_name, "src", "-j", "DROP"]).status();
79 match result {
80 Ok(status) if status.success() => info!("DDoS: iptables {} rule for '{}' installed", chain, set_name),
81 Ok(status) => warn!("DDoS: iptables -I {} for '{}' failed with status {}", chain, set_name, status),
82 Err(e) => warn!("DDoS: failed to run iptables {} for '{}': {}", chain, set_name, e),
83 }
84 }
85
86 Self {
87 set_name,
88 score_threshold,
89 decay_per_second,
90 bad_request_penalty,
91 max_connections_per_ip,
92 scores: Cache::builder().time_to_idle(Duration::from_secs(idle_secs)).build(),
93 ipset_throttle: Cache::builder().time_to_live(Duration::from_secs(10)).build(),
94 connections: Mutex::new(HashMap::new()),
95 }
96 }
97
98 fn increment_score(&self, ip: &str, points: f64) -> f64 {
99 let entry = self.scores.get_with(ip.to_string(), || Arc::new(Mutex::new(DdosScore::new())));
100 entry.lock().increment(points, self.decay_per_second)
101 }
102
103 fn is_score_banned(&self, ip: &str) -> bool {
104 self.scores
105 .get(ip)
106 .map(|entry| entry.lock().current(self.decay_per_second) >= self.score_threshold)
107 .unwrap_or(false)
108 }
109
110 fn maybe_call_ipset(&self, ip: &str) {
111 {
113 if self.ipset_throttle.contains_key(ip) {
114 return;
115 }
116 self.ipset_throttle.insert(ip.to_string(), ());
117 }
118
119 {
121 let set_name = self.set_name.clone();
122 let ip = ip.to_string();
123
124 tokio::spawn(async move {
125 info!("Banning DDoS ip: {}", ip);
126 match tokio::process::Command::new("ipset").args(["add", &set_name, &ip, "--exist"]).status().await {
127 Ok(status) if status.success() => info!("DDoS: banned {} via ipset set '{}'", ip, set_name),
128 Ok(status) => warn!("DDoS: ipset add {} failed with status {}", ip, status),
129 Err(e) => warn!("DDoS: failed to run ipset for {}: {}", ip, e),
130 }
131 });
132 }
133 }
134}
135
136impl DdosProtection for IpsetDdosProtection {
137 fn allow_request(&self, ip: &str) -> bool {
138 let score = self.increment_score(ip, 1.0);
139 if score >= self.score_threshold {
140 self.maybe_call_ipset(ip);
141 false
142 }
143 else {
144 true
145 }
146 }
147
148 fn report_bad_request(&self, ip: &str) {
149 let score = self.increment_score(ip, self.bad_request_penalty);
150 if score >= self.score_threshold {
151 self.maybe_call_ipset(ip);
152 }
153 }
154
155 fn try_acquire_connection(&self, ip: &str) -> bool {
156 if self.is_score_banned(ip) {
157 return false;
158 }
159 let mut connections = self.connections.lock();
160 let count = connections.entry(ip.to_string()).or_insert(0);
161 if *count >= self.max_connections_per_ip {
162 return false;
163 }
164 *count += 1;
165 true
166 }
167
168 fn release_connection(&self, ip: &str) {
169 let mut connections = self.connections.lock();
170 if let Some(count) = connections.get_mut(ip) {
171 *count = count.saturating_sub(1);
172 if *count == 0 {
173 connections.remove(ip);
174 }
175 }
176 }
177}