2fa for unattended access

This commit is contained in:
rustdesk
2024-01-19 15:35:58 +08:00
parent 80857c22c9
commit 44e6b7dbb0
55 changed files with 673 additions and 60 deletions

View File

@@ -67,7 +67,7 @@ use std::collections::HashSet;
pub type Sender = mpsc::UnboundedSender<(Instant, Arc<Message>)>;
lazy_static::lazy_static! {
static ref LOGIN_FAILURES: Arc::<Mutex<HashMap<String, (i32, i32, i32)>>> = Default::default();
static ref LOGIN_FAILURES: [Arc::<Mutex<HashMap<String, (i32, i32, i32)>>>; 2] = Default::default();
static ref SESSIONS: Arc::<Mutex<HashMap<String, Session>>> = Default::default();
static ref ALIVE_CONNS: Arc::<Mutex<Vec<i32>>> = Default::default();
static ref AUTHED_CONNS: Arc::<Mutex<Vec<(i32, AuthConnType)>>> = Default::default();
@@ -150,6 +150,7 @@ struct Session {
session_id: u64,
last_recv_time: Arc<Mutex<Instant>>,
random_password: String,
tfa: bool,
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -181,6 +182,7 @@ pub struct Connection {
port_forward_address: String,
tx_to_cm: mpsc::UnboundedSender<ipc::Data>,
authorized: bool,
require_2fa: Option<totp_rs::TOTP>,
keyboard: bool,
clipboard: bool,
audio: bool,
@@ -317,6 +319,7 @@ impl Connection {
tx: Some(tx),
tx_video: Some(tx_video),
},
require_2fa: crate::auth_2fa::get_2fa(None),
display_idx: *display_service::PRIMARY_DISPLAY_IDX,
stream,
server,
@@ -434,6 +437,7 @@ impl Connection {
Some(data) = rx_from_cm.recv() => {
match data {
ipc::Data::Authorize => {
conn.require_2fa.take();
conn.send_logon_response().await;
if conn.port_forward_socket.is_some() {
break;
@@ -1029,6 +1033,10 @@ impl Connection {
if self.authorized {
return;
}
if self.require_2fa.is_some() && !self.is_recent_session(true) {
self.send_login_error(crate::client::REQUIRE_2FA).await;
return;
}
let (conn_type, auth_conn_type) = if self.file_transfer.is_some() {
(1, AuthConnType::FileTransfer)
} else if self.port_forward_socket.is_some() {
@@ -1402,6 +1410,7 @@ impl Connection {
session_id: self.lr.session_id,
last_recv_time: self.last_recv_time.clone(),
random_password: password,
tfa: false,
},
);
return true;
@@ -1415,7 +1424,7 @@ impl Connection {
false
}
fn is_recent_session(&mut self) -> bool {
fn is_recent_session(&mut self, tfa: bool) -> bool {
SESSIONS
.lock()
.unwrap()
@@ -1426,21 +1435,18 @@ impl Connection {
.get(&self.lr.my_id)
.map(|s| s.to_owned());
// last_recv_time is a mutex variable shared with connection, can be updated lively.
if let Some(session) = session {
if let Some(mut session) = session {
if session.name == self.lr.my_name
&& session.session_id == self.lr.session_id
&& !self.lr.password.is_empty()
&& self.validate_one_password(session.random_password.clone())
&& (tfa && session.tfa
|| !tfa && self.validate_one_password(session.random_password.clone()))
{
SESSIONS.lock().unwrap().insert(
self.lr.my_id.clone(),
Session {
name: self.lr.my_name.clone(),
session_id: self.lr.session_id,
last_recv_time: self.last_recv_time.clone(),
random_password: session.random_password,
},
);
session.last_recv_time = self.last_recv_time.clone();
SESSIONS
.lock()
.unwrap()
.insert(self.lr.my_id.clone(), session);
return true;
}
}
@@ -1614,7 +1620,7 @@ impl Connection {
{
self.send_login_error("Connection not allowed").await;
return false;
} else if self.is_recent_session() {
} else if self.is_recent_session(false) {
if err_msg.is_empty() {
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
@@ -1637,47 +1643,12 @@ impl Connection {
.await;
}
} else {
let mut failure = LOGIN_FAILURES
.lock()
.unwrap()
.get(&self.ip)
.map(|x| x.clone())
.unwrap_or((0, 0, 0));
let time = (get_time() / 60_000) as i32;
if failure.2 > 30 {
self.send_login_error("Too many wrong password attempts")
.await;
Self::post_alarm_audit(
AlarmAuditType::ExceedThirtyAttempts,
json!({
"ip":self.ip,
"id":lr.my_id.clone(),
"name": lr.my_name.clone(),
}),
);
} else if time == failure.0 && failure.1 > 6 {
self.send_login_error("Please try 1 minute later").await;
Self::post_alarm_audit(
AlarmAuditType::SixAttemptsWithinOneMinute,
json!({
"ip":self.ip,
"id":lr.my_id.clone(),
"name": lr.my_name.clone(),
}),
);
} else if !self.validate_password() {
if failure.0 == time {
failure.1 += 1;
failure.2 += 1;
} else {
failure.0 = time;
failure.1 = 1;
failure.2 += 1;
}
LOGIN_FAILURES
.lock()
.unwrap()
.insert(self.ip.clone(), failure);
let (failure, res) = self.check_failure(0).await;
if !res {
return true;
}
if !self.validate_password() {
self.update_failure(failure, false, 0);
if err_msg.is_empty() {
self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
.await;
@@ -1689,9 +1660,7 @@ impl Connection {
.await;
}
} else {
if failure.0 != 0 {
LOGIN_FAILURES.lock().unwrap().remove(&self.ip);
}
self.update_failure(failure, true, 0);
if err_msg.is_empty() {
#[cfg(all(target_os = "linux", feature = "linux_headless"))]
#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
@@ -1706,6 +1675,47 @@ impl Connection {
}
}
}
} else if let Some(message::Union::Auth2fa(tfa)) = msg.union {
let (failure, res) = self.check_failure(1).await;
if !res {
return true;
}
if let Some(totp) = self.require_2fa.as_ref() {
if let Ok(code) = totp.generate_current() {
if tfa.code == code {
self.update_failure(failure, true, 1);
self.require_2fa.take();
self.send_logon_response().await;
let session = SESSIONS
.lock()
.unwrap()
.get(&self.lr.my_id)
.map(|s| s.to_owned());
if let Some(mut session) = session {
session.tfa = true;
SESSIONS
.lock()
.unwrap()
.insert(self.lr.my_id.clone(), session);
} else {
SESSIONS.lock().unwrap().insert(
self.lr.my_id.clone(),
Session {
name: self.lr.my_name.clone(),
session_id: self.lr.session_id,
last_recv_time: self.last_recv_time.clone(),
random_password: "".to_owned(),
tfa: true,
},
);
}
} else {
self.update_failure(failure, false, 1);
self.send_login_error(crate::client::LOGIN_MSG_2FA_WRONG)
.await;
}
}
}
} else if let Some(message::Union::TestDelay(t)) = msg.union {
if t.from_client {
let mut msg_out = Message::new();
@@ -2245,6 +2255,63 @@ impl Connection {
true
}
fn update_failure(&self, (mut failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) {
if remove {
if failure.0 != 0 {
LOGIN_FAILURES[i].lock().unwrap().remove(&self.ip);
}
return;
}
if failure.0 == time {
failure.1 += 1;
failure.2 += 1;
} else {
failure.0 = time;
failure.1 = 1;
failure.2 += 1;
}
LOGIN_FAILURES[i]
.lock()
.unwrap()
.insert(self.ip.clone(), failure);
}
async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) {
let failure = LOGIN_FAILURES[i]
.lock()
.unwrap()
.get(&self.ip)
.map(|x| x.clone())
.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(
AlarmAuditType::ExceedThirtyAttempts,
json!({
"ip": self.ip,
"id": self.lr.my_id.clone(),
"name": self.lr.my_name.clone(),
}),
);
false
} else if time == failure.0 && failure.1 > 6 {
self.send_login_error("Please try 1 minute later").await;
Self::post_alarm_audit(
AlarmAuditType::SixAttemptsWithinOneMinute,
json!({
"ip": self.ip,
"id": self.lr.my_id.clone(),
"name": self.lr.my_name.clone(),
}),
);
false
} else {
true
};
((failure, time), res)
}
fn refresh_video_display(&self, display: Option<usize>) {
video_service::refresh();
self.server.upgrade().map(|s| {