From 7972ac207d4ba0b7b15a7b787b0092f543ce5b6f Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Wed, 12 Nov 2025 10:14:13 -0300 Subject: [PATCH] fix: self-heal RustDesk ACL and restart --- apps/desktop/src-tauri/src/rustdesk.rs | 174 ++++++++++++++++++++----- 1 file changed, 140 insertions(+), 34 deletions(-) diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs index e38c531..9e9f97d 100644 --- a/apps/desktop/src-tauri/src/rustdesk.rs +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -364,10 +364,25 @@ fn derive_numeric_id(machine_id: &str) -> String { } fn ensure_service_running() -> Result<(), RustdeskError> { - let _ = run_sc(&["stop", SERVICE_NAME]); - thread::sleep(Duration::from_secs(2)); - let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]); - run_sc(&["start", SERVICE_NAME]) + fn start_sequence() -> Result<(), RustdeskError> { + let _ = run_sc(&["stop", SERVICE_NAME]); + thread::sleep(Duration::from_secs(2)); + let _ = run_sc(&["config", SERVICE_NAME, &format!("start= {}", "auto")]); + run_sc(&["start", SERVICE_NAME]) + } + + match start_sequence() { + Ok(_) => Ok(()), + Err(RustdeskError::CommandFailed { command, status: Some(5), .. }) => { + log_event("SC retornou acesso negado; tentando ajustar ACL do LocalService..."); + fix_localservice_acl().map_err(|error| RustdeskError::CommandFailed { + command: format!("fix_acl ({error})"), + status: Some(5), + })?; + start_sequence() + } + Err(error) => Err(error), + } } fn run_sc(args: &[&str]) -> Result<(), RustdeskError> { @@ -463,10 +478,16 @@ fn remote_id_directories() -> Vec { fn ensure_password_files(secret: &str) -> Result<(), String> { let mut errors = Vec::new(); + let mut need_acl_fix = false; + for dir in remote_id_directories() { let password_path = dir.join("RustDesk.toml"); if let Err(error) = write_toml_kv(&password_path, "password", secret) { - errors.push(format!("{} -> {}", password_path.display(), error)); + if is_localservice_permission_issue(&dir, &error) { + need_acl_fix = true; + } else { + errors.push(format!("{} -> {}", password_path.display(), error)); + } } else { log_event(&format!( "Senha escrita via fallback em {}", @@ -480,6 +501,9 @@ fn ensure_password_files(secret: &str) -> Result<(), String> { "Falha ao ajustar verification-method em {}: {error}", local_path.display() )); + if is_localservice_permission_issue(&dir, &error) { + need_acl_fix = true; + } } else { log_event(&format!( "verification-method atualizado para use-both-passwords em {}", @@ -488,6 +512,40 @@ fn ensure_password_files(secret: &str) -> Result<(), String> { } } + if need_acl_fix { + log_event("Tentando corrigir ACL do perfil LocalService via UAC..."); + fix_localservice_acl().map_err(|error| format!("fix acl: {error}"))?; + + for dir in remote_id_directories() { + if !is_localservice_dir(&dir) { + continue; + } + let password_path = dir.join("RustDesk.toml"); + if let Err(error) = write_toml_kv(&password_path, "password", secret) { + errors.push(format!("{} -> {}", password_path.display(), error)); + } else { + log_event(&format!( + "Senha escrita via fallback em {}", + password_path.display() + )); + } + + let local_path = dir.join("RustDesk_local.toml"); + if let Err(error) = write_toml_kv(&local_path, "verification-method", "use-both-passwords") { + errors.push(format!("{} -> {}", local_path.display(), error)); + log_event(&format!( + "Falha ao ajustar verification-method em {} mesmo após ACL: {error}", + local_path.display() + )); + } else { + log_event(&format!( + "verification-method atualizado para use-both-passwords em {}", + local_path.display() + )); + } + } + } + if errors.is_empty() { Ok(()) } else { @@ -561,6 +619,41 @@ fn replicate_password_artifacts() -> io::Result<()> { Ok(()) } +fn run_powershell_elevated(script: &str) -> Result<(), String> { + let tmp = env::temp_dir().join("raven_fix_acl.ps1"); + fs::write(&tmp, script).map_err(|error| format!("write ps1: {error}"))?; + let args = format!( + "-NoProfile -ExecutionPolicy Bypass -Command \"Start-Process -FilePath PowerShell -Verb RunAs -WindowStyle Hidden -Wait -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','{}'\"", + tmp.display() + ); + let status = Command::new("powershell") + .arg(args) + .creation_flags(CREATE_NO_WINDOW) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|error| format!("spawn ps: {error}"))?; + let _ = fs::remove_file(&tmp); + if status.success() { + Ok(()) + } else { + Err(format!("elevated ps exit {:?}", status.code())) + } +} + +fn fix_localservice_acl() -> Result<(), String> { + let target = r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config"; + let script = format!( + r#" +$ErrorActionPreference = 'Stop' +if (-not (Test-Path '{target}')) {{ New-Item -ItemType Directory -Force -Path '{target}' | Out-Null }} +takeown /F '{target}' /R /D Y | Out-Null +icacls '{target}' /grant *S-1-5-32-544:(OI)(CI)F /T /C | Out-Null +"# + ); + run_powershell_elevated(&script) +} + fn write_remote_id_value(path: &Path, id: &str) -> io::Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; @@ -601,38 +694,30 @@ fn write_toml_kv(path: &Path, key: &str, value: &str) -> io::Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } - let sanitized = value.replace('\'', "''"); - let replacement = format!("{key} = '{}'\n", sanitized); - if let Ok(existing) = fs::read_to_string(path) { - let mut replaced = false; - let mut buffer = String::with_capacity(existing.len() + replacement.len()); - for line in existing.lines() { - let trimmed = line.trim_start(); - if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) { - buffer.push_str(&replacement); - replaced = true; - } else { - buffer.push_str(line); - buffer.push('\n'); - } - } - if !replaced { + let sanitized = value.replace('\\', "\\\\").replace('"', "\\\""); + let replacement = format!("{key} = \"{sanitized}\"\n"); + let existing = fs::read_to_string(path).unwrap_or_default(); + let mut replaced = false; + let mut buffer = String::with_capacity(existing.len() + replacement.len()); + for line in existing.lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with(&format!("{key} ")) || trimmed.starts_with(&format!("{key}=")) { buffer.push_str(&replacement); + replaced = true; + } else { + buffer.push_str(line); + buffer.push('\n'); } - let mut file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path)?; - file.write_all(buffer.as_bytes()) - } else { - let mut file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path)?; - file.write_all(replacement.as_bytes()) } + if !replaced { + buffer.push_str(&replacement); + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path)?; + file.write_all(buffer.as_bytes()) } fn read_remote_id_from_profiles() -> Option { @@ -687,6 +772,27 @@ fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> { Ok(()) } +fn is_localservice_dir(path: &Path) -> bool { + path.as_os_str() + .to_string_lossy() + .to_lowercase() + .contains("serviceprofiles\\localservice") +} + +fn is_localservice_permission_issue(dir: &Path, error: &io::Error) -> bool { + if !is_localservice_dir(dir) { + return false; + } + if let Some(code) = error.raw_os_error() { + return code == 5 || code == 183; + } + let message = error.to_string().to_lowercase(); + message.contains("permission denied") + || message.contains("acesso negado") + || message.contains("os error 5") + || message.contains("os error 183") +} + fn hidden_command(program: impl AsRef) -> Command { let mut cmd = Command::new(program); cmd.creation_flags(CREATE_NO_WINDOW);