mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
feat: Add IPv6 prefix-based rate limiting on login failures (#13070)
Enhance security by implementing rate limiting on IPv6 prefixes (/64, /56, /48) to prevent brute force attacks that exploit cheap IPv6 address generation. * Add private get_ipv6_prefixes() to calculate network prefixes * Implement private check_failure_ipv6_prefix() for prefix-specific limits on IPv6 addresses * Refactor check_failure() and update_failure() to support both IPs and prefixes * Add ExceedIPv6PrefixAttempts to AlarmAuditType enum Signed-off-by: Michael Bacarella <m@bacarella.com>
This commit is contained in:
committed by
GitHub
parent
8d71534839
commit
a953845ba7
@@ -50,8 +50,10 @@ use serde_json::{json, value::Value};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::{
|
||||
net::Ipv6Addr,
|
||||
num::NonZeroI64,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
sync::{atomic::AtomicI64, mpsc as std_mpsc},
|
||||
};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
@@ -3173,35 +3175,134 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_failure(&self, (mut failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) {
|
||||
// Try to parse connection IP as IPv6 address, returning /64, /56, and /48 prefixes.
|
||||
// Parsing an IPv4 address just returns None.
|
||||
// note: we specifically don't use hbb_common::is_ipv6_str to avoid divergence issues
|
||||
// between its regex and the system std::net::Ipv6Addr implementation.
|
||||
fn get_ipv6_prefixes(&self) -> Option<(String, String, String)> {
|
||||
fn mask_u128(addr: u128, prefix: u8) -> u128 {
|
||||
let mask = if prefix == 0 || prefix > 128 {
|
||||
0
|
||||
} else {
|
||||
(!0u128) << (128 - prefix)
|
||||
};
|
||||
addr & mask
|
||||
}
|
||||
// eliminate zone-ids like "fe80::1%eth0"
|
||||
let ip_only = self.ip.split('%').next().unwrap_or(&self.ip).trim();
|
||||
let ip = Ipv6Addr::from_str(ip_only).ok()?;
|
||||
|
||||
let as_u128 = u128::from_be_bytes(ip.octets());
|
||||
|
||||
let p64 = Ipv6Addr::from(mask_u128(as_u128, 64).to_be_bytes()).to_string() + "/64";
|
||||
let p56 = Ipv6Addr::from(mask_u128(as_u128, 56).to_be_bytes()).to_string() + "/56";
|
||||
let p48 = Ipv6Addr::from(mask_u128(as_u128, 48).to_be_bytes()).to_string() + "/48";
|
||||
|
||||
Some((p64, p56, p48))
|
||||
}
|
||||
|
||||
fn update_failure(&self, (failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) {
|
||||
fn bump(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) {
|
||||
if cur.0 == time {
|
||||
cur.1 += 1;
|
||||
cur.2 += 1;
|
||||
} else {
|
||||
cur.0 = time;
|
||||
cur.1 = 1;
|
||||
cur.2 += 1;
|
||||
}
|
||||
cur
|
||||
}
|
||||
let map_mutex = &LOGIN_FAILURES[i];
|
||||
if remove {
|
||||
if failure.0 != 0 {
|
||||
LOGIN_FAILURES[i].lock().unwrap().remove(&self.ip);
|
||||
if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() {
|
||||
let mut m = map_mutex.lock().unwrap();
|
||||
m.remove(&p64);
|
||||
m.remove(&p56);
|
||||
m.remove(&p48);
|
||||
m.remove(&self.ip);
|
||||
} else {
|
||||
map_mutex.lock().unwrap().remove(&self.ip);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if failure.0 == time {
|
||||
failure.1 += 1;
|
||||
failure.2 += 1;
|
||||
// Bump the prefixes, fetching existing values
|
||||
if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() {
|
||||
let mut m = map_mutex.lock().unwrap();
|
||||
for key in [p64, p56, p48] {
|
||||
let cur = m.get(&key).copied().unwrap_or((0, 0, 0));
|
||||
m.insert(key, bump(cur, time));
|
||||
}
|
||||
// Update full IP: bump from the *original* passed-in failure
|
||||
m.insert(self.ip.clone(), bump(failure, time));
|
||||
} else {
|
||||
failure.0 = time;
|
||||
failure.1 = 1;
|
||||
failure.2 += 1;
|
||||
// Update full IP: bump from the *original* passed-in failure
|
||||
let mut m = map_mutex.lock().unwrap();
|
||||
m.insert(self.ip.clone(), bump(failure, time));
|
||||
}
|
||||
LOGIN_FAILURES[i]
|
||||
}
|
||||
|
||||
async fn check_failure_ipv6_prefix(
|
||||
&mut self,
|
||||
i: usize,
|
||||
time: i32,
|
||||
prefix: &str,
|
||||
prefix_num: i8,
|
||||
thresh: i32,
|
||||
) -> Option<(((i32, i32, i32), i32), bool)> {
|
||||
let failure_prefix = LOGIN_FAILURES[i]
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(self.ip.clone(), failure);
|
||||
.get(prefix)
|
||||
.copied()
|
||||
.unwrap_or((0, 0, 0));
|
||||
|
||||
if failure_prefix.2 > thresh {
|
||||
self.send_login_error(format!(
|
||||
"Too many wrong attempts for IPv6 prefix /{}",
|
||||
prefix_num
|
||||
))
|
||||
.await;
|
||||
Self::post_alarm_audit(
|
||||
AlarmAuditType::ExceedIPv6PrefixAttempts,
|
||||
json!({
|
||||
"ip": self.ip,
|
||||
"id": self.lr.my_id.clone(),
|
||||
"name": self.lr.my_name.clone(),
|
||||
}),
|
||||
);
|
||||
Some(((failure_prefix, time), false))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) {
|
||||
let time = (get_time() / 60_000) as i32;
|
||||
|
||||
// IPv6 addresses are cheap to make so we check prefix/netblock as well
|
||||
if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() {
|
||||
if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p64, 64, 60).await {
|
||||
return res;
|
||||
}
|
||||
if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p56, 56, 80).await {
|
||||
return res;
|
||||
}
|
||||
if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p48, 48, 100).await {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// checks IPv6 and IPv4 direct addresses
|
||||
let failure = LOGIN_FAILURES[i]
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&self.ip)
|
||||
.map(|x| x.clone())
|
||||
.copied()
|
||||
.unwrap_or((0, 0, 0));
|
||||
let time = (get_time() / 60_000) as i32;
|
||||
|
||||
let res = if failure.2 > 30 {
|
||||
self.send_login_error("Too many wrong attempts").await;
|
||||
Self::post_alarm_audit(
|
||||
@@ -4377,6 +4478,7 @@ pub enum AlarmAuditType {
|
||||
IpWhitelist = 0,
|
||||
ExceedThirtyAttempts = 1,
|
||||
SixAttemptsWithinOneMinute = 2,
|
||||
ExceedIPv6PrefixAttempts = 3,
|
||||
}
|
||||
|
||||
pub enum FileAuditType {
|
||||
@@ -4942,4 +5044,11 @@ mod test {
|
||||
assert_eq!(pos.x, 510);
|
||||
assert_eq!(pos.y, 510);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6() {
|
||||
assert!(Ipv6Addr::from_str("::1").is_ok());
|
||||
assert!(Ipv6Addr::from_str("127.0.0.1").is_err());
|
||||
assert!(Ipv6Addr::from_str("0").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user