mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-08 07:21:27 +03: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")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::{
|
use std::{
|
||||||
|
net::Ipv6Addr,
|
||||||
num::NonZeroI64,
|
num::NonZeroI64,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
str::FromStr,
|
||||||
sync::{atomic::AtomicI64, mpsc as std_mpsc},
|
sync::{atomic::AtomicI64, mpsc as std_mpsc},
|
||||||
};
|
};
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[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 remove {
|
||||||
if failure.0 != 0 {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if failure.0 == time {
|
// Bump the prefixes, fetching existing values
|
||||||
failure.1 += 1;
|
if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() {
|
||||||
failure.2 += 1;
|
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 {
|
} else {
|
||||||
failure.0 = time;
|
// Update full IP: bump from the *original* passed-in failure
|
||||||
failure.1 = 1;
|
let mut m = map_mutex.lock().unwrap();
|
||||||
failure.2 += 1;
|
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()
|
.lock()
|
||||||
.unwrap()
|
.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) {
|
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]
|
let failure = LOGIN_FAILURES[i]
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(&self.ip)
|
.get(&self.ip)
|
||||||
.map(|x| x.clone())
|
.copied()
|
||||||
.unwrap_or((0, 0, 0));
|
.unwrap_or((0, 0, 0));
|
||||||
let time = (get_time() / 60_000) as i32;
|
|
||||||
let res = if failure.2 > 30 {
|
let res = if failure.2 > 30 {
|
||||||
self.send_login_error("Too many wrong attempts").await;
|
self.send_login_error("Too many wrong attempts").await;
|
||||||
Self::post_alarm_audit(
|
Self::post_alarm_audit(
|
||||||
@@ -4377,6 +4478,7 @@ pub enum AlarmAuditType {
|
|||||||
IpWhitelist = 0,
|
IpWhitelist = 0,
|
||||||
ExceedThirtyAttempts = 1,
|
ExceedThirtyAttempts = 1,
|
||||||
SixAttemptsWithinOneMinute = 2,
|
SixAttemptsWithinOneMinute = 2,
|
||||||
|
ExceedIPv6PrefixAttempts = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum FileAuditType {
|
pub enum FileAuditType {
|
||||||
@@ -4942,4 +5044,11 @@ mod test {
|
|||||||
assert_eq!(pos.x, 510);
|
assert_eq!(pos.x, 510);
|
||||||
assert_eq!(pos.y, 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