Hardening RustDesk provisioning flow
This commit is contained in:
parent
f3d622eedd
commit
ddcff6768d
2 changed files with 184 additions and 46 deletions
|
|
@ -6,6 +6,7 @@ use once_cell::sync::Lazy;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::{Map as JsonMap, Value as JsonValue};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
|
@ -27,6 +28,10 @@ const SERVICE_NAME: &str = "RustDesk";
|
||||||
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
|
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
|
||||||
const LOCAL_SERVICE_CONFIG: &str = r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config";
|
const LOCAL_SERVICE_CONFIG: &str = r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config";
|
||||||
const LOCAL_SYSTEM_CONFIG: &str = r"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config";
|
const LOCAL_SYSTEM_CONFIG: &str = r"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config";
|
||||||
|
const APP_IDENTIFIER: &str = "br.com.esdrasrenan.sistemadechamados";
|
||||||
|
const MACHINE_STORE_FILENAME: &str = "machine-agent.json";
|
||||||
|
const ACL_FLAG_FILENAME: &str = "rustdesk_acl_unlocked.flag";
|
||||||
|
const RUSTDESK_ACL_STORE_KEY: &str = "rustdeskAclUnlockedAt";
|
||||||
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
|
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
|
||||||
const SECURITY_APPROVE_MODE_VALUE: &str = "password";
|
const SECURITY_APPROVE_MODE_VALUE: &str = "password";
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
@ -81,6 +86,13 @@ pub fn ensure_rustdesk(
|
||||||
"RustDesk já instalado, usando binário existente"
|
"RustDesk já instalado, usando binário existente"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
match stop_rustdesk_processes() {
|
||||||
|
Ok(_) => log_event("Instâncias existentes do RustDesk encerradas"),
|
||||||
|
Err(error) => log_event(&format!(
|
||||||
|
"Aviso: não foi possível parar completamente o RustDesk antes da reprovisionamento ({error})"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(value) = config_string.and_then(|raw| {
|
if let Some(value) = config_string.and_then(|raw| {
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
if trimmed.is_empty() { None } else { Some(trimmed) }
|
if trimmed.is_empty() { None } else { Some(trimmed) }
|
||||||
|
|
@ -120,19 +132,15 @@ pub fn ensure_rustdesk(
|
||||||
log_event(&format!("Falha ao ajustar approve-mode via CLI: {error}"));
|
log_event(&format!("Falha ao ajustar approve-mode via CLI: {error}"));
|
||||||
}
|
}
|
||||||
match propagate_password_profile() {
|
match propagate_password_profile() {
|
||||||
Ok(true) => log_event("Perfil de senha propagado para ProgramData/LocalService"),
|
Ok(_) => log_event("Perfil base propagado para ProgramData e perfis de serviço"),
|
||||||
Ok(false) => match ensure_password_files(&password) {
|
Err(error) => log_event(&format!("Falha ao copiar perfil de senha: {error}")),
|
||||||
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
|
}
|
||||||
Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
|
|
||||||
},
|
|
||||||
Err(error) => {
|
|
||||||
log_event(&format!("Falha ao copiar perfil de senha: {error}"));
|
|
||||||
match ensure_password_files(&password) {
|
match ensure_password_files(&password) {
|
||||||
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
|
Ok(_) => log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"),
|
||||||
Err(inner) => log_event(&format!("Falha ao persistir senha nos perfis: {inner}")),
|
Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match replicate_password_artifacts() {
|
match replicate_password_artifacts() {
|
||||||
Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"),
|
Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"),
|
||||||
Err(error) => log_event(&format!("Falha ao replicar artefatos de senha: {error}")),
|
Err(error) => log_event(&format!("Falha ao replicar artefatos de senha: {error}")),
|
||||||
|
|
@ -272,13 +280,16 @@ fn write_config_files() -> Result<PathBuf, RustdeskError> {
|
||||||
main_path.display()
|
main_path.display()
|
||||||
));
|
));
|
||||||
|
|
||||||
let service_profile = PathBuf::from(LOCAL_SERVICE_CONFIG).join("RustDesk2.toml");
|
|
||||||
let _ = ensure_service_profiles_writable_preflight();
|
let _ = ensure_service_profiles_writable_preflight();
|
||||||
|
for service_dir in service_profile_dirs() {
|
||||||
|
let service_profile = service_dir.join("RustDesk2.toml");
|
||||||
if let Err(error) = write_file(&service_profile, &config_contents) {
|
if let Err(error) = write_file(&service_profile, &config_contents) {
|
||||||
log_event(&format!(
|
log_event(&format!(
|
||||||
"Falha ao gravar config no perfil do serviço: {error}"
|
"Falha ao gravar config no perfil do serviço ({}): {error}",
|
||||||
|
service_profile.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") {
|
if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") {
|
||||||
if let Err(error) = write_file(&appdata_path, &config_contents) {
|
if let Err(error) = write_file(&appdata_path, &config_contents) {
|
||||||
|
|
@ -402,6 +413,30 @@ fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stop_rustdesk_processes() -> Result<(), RustdeskError> {
|
||||||
|
if let Err(error) = run_sc(&["stop", SERVICE_NAME]) {
|
||||||
|
match error {
|
||||||
|
RustdeskError::CommandFailed { status: Some(code), .. } if code == 1062 || code == 1060 => {}
|
||||||
|
_ => log_event(&format!("Não foi possível parar o serviço RustDesk antes da sincronização: {error}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = hidden_command("taskkill")
|
||||||
|
.args(["/F", "/T", "/IM", "rustdesk.exe"])
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
if status.success() || matches!(status.code(), Some(128)) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(RustdeskError::CommandFailed {
|
||||||
|
command: "taskkill /F /T /IM rustdesk.exe".into(),
|
||||||
|
status: status.code(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
|
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
|
||||||
let status = hidden_command("sc")
|
let status = hidden_command("sc")
|
||||||
.args(args)
|
.args(args)
|
||||||
|
|
@ -512,6 +547,13 @@ fn service_profile_dirs() -> Vec<PathBuf> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn propagation_destinations() -> Vec<PathBuf> {
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
dirs.push(program_data_config_dir());
|
||||||
|
dirs.extend(service_profile_dirs());
|
||||||
|
dirs
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_password_files(secret: &str) -> Result<(), String> {
|
fn ensure_password_files(secret: &str) -> Result<(), String> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
|
@ -598,29 +640,36 @@ fn propagate_password_profile() -> io::Result<bool> {
|
||||||
log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)");
|
log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
let src_path = src_dir.join("RustDesk.toml");
|
|
||||||
|
let files = ["RustDesk.toml", "RustDesk_local.toml"];
|
||||||
|
let mut propagated = false;
|
||||||
|
|
||||||
|
for filename in files {
|
||||||
|
let src_path = src_dir.join(filename);
|
||||||
if !src_path.exists() {
|
if !src_path.exists() {
|
||||||
log_event(&format!(
|
continue;
|
||||||
"Arquivo {} não encontrado; usando fallback de escrita",
|
|
||||||
src_path.display()
|
|
||||||
));
|
|
||||||
return Ok(false);
|
|
||||||
}
|
}
|
||||||
log_event(&format!(
|
log_event(&format!(
|
||||||
"Copiando {} para ProgramData/LocalService",
|
"Copiando {} para ProgramData/serviços",
|
||||||
src_path.display()
|
src_path.display()
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut propagated = false;
|
for dest_root in propagation_destinations() {
|
||||||
for dest_root in [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)] {
|
let target_path = dest_root.join(filename);
|
||||||
let target_path = dest_root.join("RustDesk.toml");
|
|
||||||
copy_overwrite(&src_path, &target_path)?;
|
copy_overwrite(&src_path, &target_path)?;
|
||||||
log_event(&format!(
|
log_event(&format!(
|
||||||
"RustDesk.toml propagado para {}",
|
"{} propagado para {}",
|
||||||
|
filename,
|
||||||
target_path.display()
|
target_path.display()
|
||||||
));
|
));
|
||||||
propagated = true;
|
propagated = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !propagated {
|
||||||
|
log_event("Nenhum arquivo de perfil encontrado para propagação; aplicando fallback");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(propagated)
|
Ok(propagated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,7 +677,7 @@ fn replicate_password_artifacts() -> io::Result<()> {
|
||||||
let Some(src) = user_appdata_config_dir() else {
|
let Some(src) = user_appdata_config_dir() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let destinations = [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)];
|
let destinations = propagation_destinations();
|
||||||
let candidates = ["password", "passwd", "passwd.txt"];
|
let candidates = ["password", "passwd", "passwd.txt"];
|
||||||
|
|
||||||
for dest in destinations {
|
for dest in destinations {
|
||||||
|
|
@ -750,28 +799,38 @@ try {{
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
|
fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
|
||||||
let mut success = false;
|
let mut blocked_dirs = Vec::new();
|
||||||
let mut last_error: Option<String> = None;
|
|
||||||
|
|
||||||
for dir in service_profile_dirs() {
|
for dir in service_profile_dirs() {
|
||||||
if can_write_dir(&dir) {
|
if !can_write_dir(&dir) {
|
||||||
success = true;
|
blocked_dirs.push(dir);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if blocked_dirs.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_acl_unlock_flag() {
|
||||||
|
log_event("Perfis do serviço voltaram a bloquear escrita; reaplicando correção de ACL");
|
||||||
|
} else {
|
||||||
|
log_event("Executando ajuste inicial de ACL dos perfis do serviço (requer UAC)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_error: Option<String> = None;
|
||||||
|
for dir in blocked_dirs.iter() {
|
||||||
log_event(&format!(
|
log_event(&format!(
|
||||||
"Tentando corrigir ACL via UAC (preflight) em {}...",
|
"Tentando corrigir ACL via UAC (preflight) em {}...",
|
||||||
dir.display()
|
dir.display()
|
||||||
));
|
));
|
||||||
if let Err(error) = fix_profile_acl(&dir) {
|
if let Err(error) = fix_profile_acl(dir) {
|
||||||
last_error = Some(error);
|
last_error = Some(error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if can_write_dir(&dir) {
|
if can_write_dir(dir) {
|
||||||
log_event(&format!(
|
log_event(&format!(
|
||||||
"ACL ajustada com sucesso em {}",
|
"ACL ajustada com sucesso em {}",
|
||||||
dir.display()
|
dir.display()
|
||||||
));
|
));
|
||||||
success = true;
|
|
||||||
} else {
|
} else {
|
||||||
last_error = Some(format!(
|
last_error = Some(format!(
|
||||||
"continua sem permissão para {} mesmo após preflight",
|
"continua sem permissão para {} mesmo após preflight",
|
||||||
|
|
@ -780,7 +839,8 @@ fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if success {
|
if blocked_dirs.iter().all(|dir| can_write_dir(dir)) {
|
||||||
|
mark_acl_unlock_flag();
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(last_error.unwrap_or_else(|| "nenhum perfil de serviço acessível".into()))
|
Err(last_error.unwrap_or_else(|| "nenhum perfil de serviço acessível".into()))
|
||||||
|
|
@ -978,3 +1038,77 @@ fn append_log(dir: PathBuf, message: &str) -> io::Result<()> {
|
||||||
writeln!(file, "[{timestamp}] {message}")?;
|
writeln!(file, "[{timestamp}] {message}")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn raven_appdata_root() -> Option<PathBuf> {
|
||||||
|
env::var("LOCALAPPDATA")
|
||||||
|
.ok()
|
||||||
|
.map(|value| Path::new(&value).join(APP_IDENTIFIER))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn machine_store_path() -> Option<PathBuf> {
|
||||||
|
raven_appdata_root().map(|dir| dir.join(MACHINE_STORE_FILENAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_machine_store_object() -> Option<JsonMap<String, JsonValue>> {
|
||||||
|
let path = machine_store_path()?;
|
||||||
|
let contents = fs::read_to_string(path).ok()?;
|
||||||
|
let value: JsonValue = serde_json::from_str(&contents).ok()?;
|
||||||
|
value.as_object().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_machine_store_object(map: JsonMap<String, JsonValue>) -> Result<(), String> {
|
||||||
|
let path = machine_store_path().ok_or_else(|| "LOCALAPPDATA não disponível".to_string())?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|error| format!("mkdir AppData: {error}"))?;
|
||||||
|
}
|
||||||
|
let serialized = serde_json::to_vec_pretty(&JsonValue::Object(map))
|
||||||
|
.map_err(|error| format!("serialize machine-agent: {error}"))?;
|
||||||
|
fs::write(&path, serialized).map_err(|error| format!("write machine-agent: {error}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upsert_machine_store_value(key: &str, value: JsonValue) -> Result<(), String> {
|
||||||
|
let mut map = read_machine_store_object().unwrap_or_else(JsonMap::new);
|
||||||
|
map.insert(key.to_string(), value);
|
||||||
|
write_machine_store_object(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn machine_store_key_exists(key: &str) -> bool {
|
||||||
|
read_machine_store_object()
|
||||||
|
.map(|map| map.contains_key(key))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acl_flag_file_path() -> Option<PathBuf> {
|
||||||
|
raven_appdata_root().map(|dir| dir.join(ACL_FLAG_FILENAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_acl_unlock_flag() -> bool {
|
||||||
|
if let Some(flag) = acl_flag_file_path() {
|
||||||
|
if flag.exists() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
machine_store_key_exists(RUSTDESK_ACL_STORE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_acl_unlock_flag() {
|
||||||
|
let timestamp = Utc::now().timestamp_millis();
|
||||||
|
if let Some(flag_path) = acl_flag_file_path() {
|
||||||
|
if let Some(parent) = flag_path.parent() {
|
||||||
|
let _ = fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
if let Err(error) = fs::write(&flag_path, timestamp.to_string()) {
|
||||||
|
log_event(&format!(
|
||||||
|
"Falha ao gravar flag de ACL em {}: {error}",
|
||||||
|
flag_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = upsert_machine_store_value(RUSTDESK_ACL_STORE_KEY, JsonValue::from(timestamp)) {
|
||||||
|
log_event(&format!(
|
||||||
|
"Falha ao registrar flag de ACL no machine-agent: {error}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ Fluxo ideal:
|
||||||
- Instala/atualiza binário, configura `RustDesk2.toml` com `relay-server`, `api-server`, etc.
|
- Instala/atualiza binário, configura `RustDesk2.toml` com `relay-server`, `api-server`, etc.
|
||||||
- Define ID determinístico (hash do `machine_id`, mas depois compara com `--get-id` e usa o “reportado” caso o serviço tenha um ID próprio).
|
- Define ID determinístico (hash do `machine_id`, mas depois compara com `--get-id` e usa o “reportado” caso o serviço tenha um ID próprio).
|
||||||
- Reinicia serviço `RustDesk` (`sc start RustDesk` — pode falhar com `status Some(5)` se não há privilégio admin). Mesmo com falha, a CLI continua e grava o ID nos arquivos.
|
- Reinicia serviço `RustDesk` (`sc start RustDesk` — pode falhar com `status Some(5)` se não há privilégio admin). Mesmo com falha, a CLI continua e grava o ID nos arquivos.
|
||||||
|
- **Autoelevação única**: na primeira execução do botão “Preparar”, o Raven dispara um PowerShell elevado (`takeown + icacls`) para liberar ACL dos perfis `LocalService` e `LocalSystem`. O sucesso grava `rustdeskAclUnlockedAt` dentro de `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\machine-agent.json` e cria o flag `rustdesk_acl_unlocked.flag`, evitando novos prompts de UAC nas execuções seguintes.
|
||||||
|
- **Kill/restart seguro**: antes de tocar nos TOML, o Raven roda `sc stop RustDesk` + `taskkill /F /T /IM rustdesk.exe`. Isso garante que nenhum cliente sobrescreva o `RustDesk_local.toml` enquanto aplicamos a senha.
|
||||||
|
- **Replicação completa de perfis**: após aplicar `--password`, copiamos `RustDesk.toml` e `RustDesk_local.toml` do `%APPDATA%` para `C:\ProgramData\RustDesk\config`, `LocalService` e `LocalSystem`. Em seguida, escrevemos (via `write_toml_kv`) `password`, `verification-method = "use-permanent-password"` e `approve-mode = "password"` em **todos** os perfis, além de replicar `password/passwd/passwd.txt`.
|
||||||
|
- **Reforço contínuo**: toda nova execução do botão “Preparar” repete o ciclo (kill → copiar → gravar flags → `sc start`). Se alguém voltar manualmente para “Use both”, o Raven mata o processo, reescreve os TOML e reinicia o serviço — o RustDesk volta travado na senha permanente com o PIN registrado nos logs (`rustdesk.log`).
|
||||||
- **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos:
|
- **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos:
|
||||||
```ts
|
```ts
|
||||||
if (response.status === 401/500 contendo "token revogado") {
|
if (response.status === 401/500 contendo "token revogado") {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue