Add SAS Service for Ctrl+Alt+Del support
- New guruconnect-sas-service binary (runs as SYSTEM) - Named pipe IPC for agent-to-service communication - Multi-tier SAS approach: service > sas.dll > fallback - Service auto-install/uninstall helpers in startup.rs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,8 @@ windows = { version = "0.58", features = [
|
||||
"Win32_System_Console",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_SystemServices",
|
||||
]}
|
||||
|
||||
# Windows service support
|
||||
@@ -84,6 +86,14 @@ windows-service = "0.7"
|
||||
prost-build = "0.13"
|
||||
winres = "0.1"
|
||||
|
||||
[[bin]]
|
||||
name = "guruconnect-agent"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "guruconnect-sas-service"
|
||||
path = "src/bin/sas_service.rs"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
587
agent/src/bin/sas_service.rs
Normal file
587
agent/src/bin/sas_service.rs
Normal file
@@ -0,0 +1,587 @@
|
||||
//! GuruConnect SAS Service
|
||||
//!
|
||||
//! Windows Service running as SYSTEM to handle Ctrl+Alt+Del (Secure Attention Sequence).
|
||||
//! The agent communicates with this service via named pipe IPC.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use windows::core::{s, w, PCSTR};
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
|
||||
use windows::Win32::Security::{
|
||||
InitializeSecurityDescriptor, SetSecurityDescriptorDacl, PSECURITY_DESCRIPTOR,
|
||||
SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR,
|
||||
};
|
||||
use windows::Win32::Storage::FileSystem::{
|
||||
FlushFileBuffers, ReadFile, WriteFile,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
|
||||
use windows::Win32::System::Pipes::{
|
||||
ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, PIPE_ACCESS_DUPLEX,
|
||||
PIPE_READMODE_MESSAGE, PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT,
|
||||
};
|
||||
use windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION;
|
||||
use windows_service::{
|
||||
define_windows_service,
|
||||
service::{
|
||||
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode,
|
||||
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
|
||||
},
|
||||
service_control_handler::{self, ServiceControlHandlerResult},
|
||||
service_dispatcher,
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
// Service configuration
|
||||
const SERVICE_NAME: &str = "GuruConnectSAS";
|
||||
const SERVICE_DISPLAY_NAME: &str = "GuruConnect SAS Service";
|
||||
const SERVICE_DESCRIPTION: &str = "Handles Secure Attention Sequence (Ctrl+Alt+Del) for GuruConnect remote sessions";
|
||||
const PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas";
|
||||
const INSTALL_DIR: &str = r"C:\Program Files\GuruConnect";
|
||||
|
||||
fn main() {
|
||||
// Set up logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_target(false)
|
||||
.init();
|
||||
|
||||
match std::env::args().nth(1).as_deref() {
|
||||
Some("install") => {
|
||||
if let Err(e) = install_service() {
|
||||
eprintln!("Failed to install service: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("uninstall") => {
|
||||
if let Err(e) = uninstall_service() {
|
||||
eprintln!("Failed to uninstall service: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("start") => {
|
||||
if let Err(e) = start_service() {
|
||||
eprintln!("Failed to start service: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("stop") => {
|
||||
if let Err(e) = stop_service() {
|
||||
eprintln!("Failed to stop service: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("status") => {
|
||||
if let Err(e) = query_status() {
|
||||
eprintln!("Failed to query status: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("service") => {
|
||||
// Called by SCM when service starts
|
||||
if let Err(e) = run_as_service() {
|
||||
eprintln!("Service error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("test") => {
|
||||
// Test mode: run pipe server directly (for debugging)
|
||||
println!("Running in test mode (not as service)...");
|
||||
if let Err(e) = run_pipe_server() {
|
||||
eprintln!("Pipe server error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print_usage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
println!("GuruConnect SAS Service");
|
||||
println!();
|
||||
println!("Usage: guruconnect-sas-service <command>");
|
||||
println!();
|
||||
println!("Commands:");
|
||||
println!(" install Install the service");
|
||||
println!(" uninstall Remove the service");
|
||||
println!(" start Start the service");
|
||||
println!(" stop Stop the service");
|
||||
println!(" status Query service status");
|
||||
println!(" test Run in test mode (not as service)");
|
||||
}
|
||||
|
||||
// Generate the Windows service boilerplate
|
||||
define_windows_service!(ffi_service_main, service_main);
|
||||
|
||||
/// Entry point called by the Windows Service Control Manager
|
||||
fn run_as_service() -> Result<()> {
|
||||
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
||||
.context("Failed to start service dispatcher")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main service function called by the SCM
|
||||
fn service_main(_arguments: Vec<OsString>) {
|
||||
if let Err(e) = run_service() {
|
||||
tracing::error!("Service error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual service implementation
|
||||
fn run_service() -> Result<()> {
|
||||
// Create a channel to receive stop events
|
||||
let (shutdown_tx, shutdown_rx) = mpsc::channel();
|
||||
|
||||
// Create the service control handler
|
||||
let event_handler = move |control_event| -> ServiceControlHandlerResult {
|
||||
match control_event {
|
||||
ServiceControl::Stop | ServiceControl::Shutdown => {
|
||||
tracing::info!("Received stop/shutdown command");
|
||||
let _ = shutdown_tx.send(());
|
||||
ServiceControlHandlerResult::NoError
|
||||
}
|
||||
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||
_ => ServiceControlHandlerResult::NotImplemented,
|
||||
}
|
||||
};
|
||||
|
||||
// Register the service control handler
|
||||
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)
|
||||
.context("Failed to register service control handler")?;
|
||||
|
||||
// Report that we're starting
|
||||
status_handle
|
||||
.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: ServiceState::StartPending,
|
||||
controls_accepted: ServiceControlAccept::empty(),
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::from_secs(5),
|
||||
process_id: None,
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Report that we're running
|
||||
status_handle
|
||||
.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: ServiceState::Running,
|
||||
controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN,
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
})
|
||||
.ok();
|
||||
|
||||
tracing::info!("GuruConnect SAS Service started");
|
||||
|
||||
// Run the pipe server in a separate thread
|
||||
let pipe_handle = std::thread::spawn(|| {
|
||||
if let Err(e) = run_pipe_server() {
|
||||
tracing::error!("Pipe server error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for shutdown signal
|
||||
let _ = shutdown_rx.recv();
|
||||
|
||||
tracing::info!("Shutting down...");
|
||||
|
||||
// Report that we're stopping
|
||||
status_handle
|
||||
.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: ServiceState::StopPending,
|
||||
controls_accepted: ServiceControlAccept::empty(),
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::from_secs(3),
|
||||
process_id: None,
|
||||
})
|
||||
.ok();
|
||||
|
||||
// The pipe thread will exit when the service stops
|
||||
drop(pipe_handle);
|
||||
|
||||
// Report stopped
|
||||
status_handle
|
||||
.set_service_status(ServiceStatus {
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
current_state: ServiceState::Stopped,
|
||||
controls_accepted: ServiceControlAccept::empty(),
|
||||
exit_code: ServiceExitCode::Win32(0),
|
||||
checkpoint: 0,
|
||||
wait_hint: Duration::default(),
|
||||
process_id: None,
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the named pipe server
|
||||
fn run_pipe_server() -> Result<()> {
|
||||
tracing::info!("Starting pipe server on {}", PIPE_NAME);
|
||||
|
||||
loop {
|
||||
// Create the pipe with security that allows all authenticated users
|
||||
let pipe = unsafe {
|
||||
// Create a security descriptor that allows everyone
|
||||
let mut sd = SECURITY_DESCRIPTOR::default();
|
||||
InitializeSecurityDescriptor(
|
||||
PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _),
|
||||
SECURITY_DESCRIPTOR_REVISION,
|
||||
)?;
|
||||
|
||||
// Set NULL DACL = allow everyone
|
||||
SetSecurityDescriptorDacl(
|
||||
PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _),
|
||||
true,
|
||||
None,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let mut sa = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: &mut sd as *mut _ as *mut _,
|
||||
bInheritHandle: false.into(),
|
||||
};
|
||||
|
||||
let pipe_name: Vec<u16> = PIPE_NAME.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
|
||||
CreateNamedPipeW(
|
||||
windows::core::PCWSTR(pipe_name.as_ptr()),
|
||||
PIPE_ACCESS_DUPLEX,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
512,
|
||||
512,
|
||||
0,
|
||||
Some(&mut sa),
|
||||
)
|
||||
};
|
||||
|
||||
if pipe == INVALID_HANDLE_VALUE {
|
||||
tracing::error!("Failed to create named pipe");
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!("Waiting for client connection...");
|
||||
|
||||
// Wait for a client to connect
|
||||
let connected = unsafe { ConnectNamedPipe(pipe, None) };
|
||||
if connected.is_err() {
|
||||
// ERROR_PIPE_CONNECTED means client connected between CreateNamedPipe and ConnectNamedPipe
|
||||
// That's OK, continue processing
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() != Some(535) {
|
||||
// 535 = ERROR_PIPE_CONNECTED
|
||||
tracing::warn!("ConnectNamedPipe error: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Client connected");
|
||||
|
||||
// Read command from pipe
|
||||
let mut buffer = [0u8; 512];
|
||||
let mut bytes_read = 0u32;
|
||||
|
||||
let read_result = unsafe {
|
||||
ReadFile(
|
||||
pipe,
|
||||
Some(&mut buffer),
|
||||
Some(&mut bytes_read),
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
if read_result.is_ok() && bytes_read > 0 {
|
||||
let command = String::from_utf8_lossy(&buffer[..bytes_read as usize]);
|
||||
let command = command.trim();
|
||||
|
||||
tracing::info!("Received command: {}", command);
|
||||
|
||||
let response = match command {
|
||||
"sas" => {
|
||||
match send_sas() {
|
||||
Ok(()) => {
|
||||
tracing::info!("SendSAS executed successfully");
|
||||
"ok\n"
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("SendSAS failed: {}", e);
|
||||
"error\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
"ping" => {
|
||||
tracing::info!("Ping received");
|
||||
"pong\n"
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unknown command: {}", command);
|
||||
"unknown\n"
|
||||
}
|
||||
};
|
||||
|
||||
// Write response
|
||||
let mut bytes_written = 0u32;
|
||||
let _ = unsafe {
|
||||
WriteFile(
|
||||
pipe,
|
||||
Some(response.as_bytes()),
|
||||
Some(&mut bytes_written),
|
||||
None,
|
||||
)
|
||||
};
|
||||
let _ = unsafe { FlushFileBuffers(pipe) };
|
||||
}
|
||||
|
||||
// Disconnect and close the pipe
|
||||
unsafe {
|
||||
DisconnectNamedPipe(pipe);
|
||||
CloseHandle(pipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call SendSAS via sas.dll
|
||||
fn send_sas() -> Result<()> {
|
||||
unsafe {
|
||||
let lib = LoadLibraryW(w!("sas.dll")).context("Failed to load sas.dll")?;
|
||||
|
||||
let proc = GetProcAddress(lib, s!("SendSAS"));
|
||||
if proc.is_none() {
|
||||
anyhow::bail!("SendSAS function not found in sas.dll");
|
||||
}
|
||||
|
||||
// SendSAS takes a BOOL parameter: FALSE (0) = Ctrl+Alt+Del
|
||||
type SendSASFn = unsafe extern "system" fn(i32);
|
||||
let send_sas_fn: SendSASFn = std::mem::transmute(proc.unwrap());
|
||||
|
||||
tracing::info!("Calling SendSAS(0)...");
|
||||
send_sas_fn(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the service
|
||||
fn install_service() -> Result<()> {
|
||||
println!("Installing GuruConnect SAS Service...");
|
||||
|
||||
// Get current executable path
|
||||
let current_exe = std::env::current_exe().context("Failed to get current executable")?;
|
||||
|
||||
let binary_dest = std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR));
|
||||
|
||||
// Create install directory
|
||||
std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?;
|
||||
|
||||
// Copy binary
|
||||
println!("Copying binary to: {:?}", binary_dest);
|
||||
std::fs::copy(¤t_exe, &binary_dest).context("Failed to copy binary")?;
|
||||
|
||||
// Open service manager
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
|
||||
// Check if service exists and remove it
|
||||
if let Ok(service) = manager.open_service(
|
||||
SERVICE_NAME,
|
||||
ServiceAccess::QUERY_STATUS | ServiceAccess::DELETE | ServiceAccess::STOP,
|
||||
) {
|
||||
println!("Removing existing service...");
|
||||
|
||||
if let Ok(status) = service.query_status() {
|
||||
if status.current_state != ServiceState::Stopped {
|
||||
let _ = service.stop();
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
}
|
||||
}
|
||||
|
||||
service.delete().context("Failed to delete existing service")?;
|
||||
drop(service);
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
}
|
||||
|
||||
// Create the service
|
||||
let service_info = ServiceInfo {
|
||||
name: OsString::from(SERVICE_NAME),
|
||||
display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
start_type: ServiceStartType::AutoStart,
|
||||
error_control: ServiceErrorControl::Normal,
|
||||
executable_path: binary_dest.clone(),
|
||||
launch_arguments: vec![OsString::from("service")],
|
||||
dependencies: vec![],
|
||||
account_name: None, // LocalSystem
|
||||
account_password: None,
|
||||
};
|
||||
|
||||
let service = manager
|
||||
.create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START)
|
||||
.context("Failed to create service")?;
|
||||
|
||||
// Set description
|
||||
service
|
||||
.set_description(SERVICE_DESCRIPTION)
|
||||
.context("Failed to set service description")?;
|
||||
|
||||
// Configure recovery
|
||||
let _ = std::process::Command::new("sc")
|
||||
.args([
|
||||
"failure",
|
||||
SERVICE_NAME,
|
||||
"reset=86400",
|
||||
"actions=restart/5000/restart/5000/restart/5000",
|
||||
])
|
||||
.output();
|
||||
|
||||
println!("\n** GuruConnect SAS Service installed successfully!");
|
||||
println!("\nBinary: {:?}", binary_dest);
|
||||
println!("\nStarting service...");
|
||||
|
||||
// Start the service
|
||||
start_service()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uninstall the service
|
||||
fn uninstall_service() -> Result<()> {
|
||||
println!("Uninstalling GuruConnect SAS Service...");
|
||||
|
||||
let binary_path = std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR));
|
||||
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager. Run as Administrator.")?;
|
||||
|
||||
match manager.open_service(
|
||||
SERVICE_NAME,
|
||||
ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE,
|
||||
) {
|
||||
Ok(service) => {
|
||||
if let Ok(status) = service.query_status() {
|
||||
if status.current_state != ServiceState::Stopped {
|
||||
println!("Stopping service...");
|
||||
let _ = service.stop();
|
||||
std::thread::sleep(Duration::from_secs(3));
|
||||
}
|
||||
}
|
||||
|
||||
println!("Deleting service...");
|
||||
service.delete().context("Failed to delete service")?;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Service was not installed");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove binary
|
||||
if binary_path.exists() {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
if let Err(e) = std::fs::remove_file(&binary_path) {
|
||||
println!("Warning: Failed to remove binary: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n** GuruConnect SAS Service uninstalled successfully!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the service
|
||||
fn start_service() -> Result<()> {
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
|
||||
let service = manager
|
||||
.open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS)
|
||||
.context("Failed to open service. Is it installed?")?;
|
||||
|
||||
service.start::<String>(&[]).context("Failed to start service")?;
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
let status = service.query_status()?;
|
||||
match status.current_state {
|
||||
ServiceState::Running => println!("** Service started successfully"),
|
||||
ServiceState::StartPending => println!("** Service is starting..."),
|
||||
other => println!("Service state: {:?}", other),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the service
|
||||
fn stop_service() -> Result<()> {
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
|
||||
let service = manager
|
||||
.open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS)
|
||||
.context("Failed to open service")?;
|
||||
|
||||
service.stop().context("Failed to stop service")?;
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
let status = service.query_status()?;
|
||||
match status.current_state {
|
||||
ServiceState::Stopped => println!("** Service stopped"),
|
||||
ServiceState::StopPending => println!("** Service is stopping..."),
|
||||
other => println!("Service state: {:?}", other),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query service status
|
||||
fn query_status() -> Result<()> {
|
||||
let manager = ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::CONNECT,
|
||||
)
|
||||
.context("Failed to connect to Service Control Manager")?;
|
||||
|
||||
match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) {
|
||||
Ok(service) => {
|
||||
let status = service.query_status()?;
|
||||
println!("GuruConnect SAS Service");
|
||||
println!("=======================");
|
||||
println!("Name: {}", SERVICE_NAME);
|
||||
println!("State: {:?}", status.current_state);
|
||||
println!("Binary: {}\\guruconnect-sas-service.exe", INSTALL_DIR);
|
||||
println!("Pipe: {}", PIPE_NAME);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("GuruConnect SAS Service");
|
||||
println!("=======================");
|
||||
println!("Status: NOT INSTALLED");
|
||||
println!("\nTo install: guruconnect-sas-service install");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -129,15 +129,21 @@ impl KeyboardController {
|
||||
|
||||
/// Send Secure Attention Sequence (Ctrl+Alt+Delete)
|
||||
///
|
||||
/// Note: This requires special privileges on Windows.
|
||||
/// The agent typically needs to run as SYSTEM or use SAS API.
|
||||
/// This uses a multi-tier approach:
|
||||
/// 1. Try the GuruConnect SAS Service (runs as SYSTEM, handles via named pipe)
|
||||
/// 2. Try the sas.dll directly (requires SYSTEM privileges)
|
||||
/// 3. Fallback to key simulation (won't work on secure desktop)
|
||||
#[cfg(windows)]
|
||||
pub fn send_sas(&mut self) -> Result<()> {
|
||||
// Try using the SAS library if available
|
||||
// For now, we'll attempt to send the key combination
|
||||
// This won't work in all contexts due to Windows security
|
||||
// Tier 1: Try the SAS service (named pipe IPC to SYSTEM service)
|
||||
if let Ok(()) = crate::sas_client::request_sas() {
|
||||
tracing::info!("SAS sent via GuruConnect SAS Service");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Load the sas.dll and call SendSAS if available
|
||||
tracing::info!("SAS service not available, trying direct sas.dll...");
|
||||
|
||||
// Tier 2: Try using the sas.dll directly (requires SYSTEM privileges)
|
||||
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
@@ -151,13 +157,14 @@ impl KeyboardController {
|
||||
// SendSAS takes a BOOL parameter: FALSE for Ctrl+Alt+Del
|
||||
let send_sas: extern "system" fn(i32) = std::mem::transmute(proc);
|
||||
send_sas(0); // FALSE = Ctrl+Alt+Del
|
||||
tracing::info!("SAS sent via direct sas.dll call");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try sending the keys (won't work without proper privileges)
|
||||
tracing::warn!("SAS library not available, Ctrl+Alt+Del may not work");
|
||||
// Tier 3: Fallback - try sending the keys (won't work on secure desktop)
|
||||
tracing::warn!("SAS service and sas.dll not available, Ctrl+Alt+Del may not work");
|
||||
|
||||
// VK codes
|
||||
const VK_CONTROL: u16 = 0x11;
|
||||
|
||||
@@ -16,6 +16,7 @@ mod chat;
|
||||
mod config;
|
||||
mod encoder;
|
||||
mod input;
|
||||
mod sas_client;
|
||||
mod session;
|
||||
mod startup;
|
||||
mod transport;
|
||||
|
||||
106
agent/src/sas_client.rs
Normal file
106
agent/src/sas_client.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
//! SAS Client - Named pipe client for communicating with GuruConnect SAS Service
|
||||
//!
|
||||
//! The SAS Service runs as SYSTEM and handles Ctrl+Alt+Del requests.
|
||||
//! This client sends commands to the service via named pipe.
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Read, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
const PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas";
|
||||
const TIMEOUT_MS: u64 = 5000;
|
||||
|
||||
/// Request Ctrl+Alt+Del (Secure Attention Sequence) via the SAS service
|
||||
pub fn request_sas() -> Result<()> {
|
||||
info!("Requesting SAS via service pipe...");
|
||||
|
||||
// Try to connect to the pipe
|
||||
let mut pipe = match OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(PIPE_NAME)
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
warn!("Failed to connect to SAS service pipe: {}", e);
|
||||
return Err(anyhow::anyhow!(
|
||||
"SAS service not available. Install with: guruconnect-sas-service install"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Connected to SAS service pipe");
|
||||
|
||||
// Send the command
|
||||
pipe.write_all(b"sas\n")
|
||||
.context("Failed to send command to SAS service")?;
|
||||
|
||||
// Read the response
|
||||
let mut response = [0u8; 64];
|
||||
let n = pipe.read(&mut response)
|
||||
.context("Failed to read response from SAS service")?;
|
||||
|
||||
let response_str = String::from_utf8_lossy(&response[..n]);
|
||||
let response_str = response_str.trim();
|
||||
|
||||
debug!("SAS service response: {}", response_str);
|
||||
|
||||
match response_str {
|
||||
"ok" => {
|
||||
info!("SAS request successful");
|
||||
Ok(())
|
||||
}
|
||||
"error" => {
|
||||
error!("SAS service reported an error");
|
||||
Err(anyhow::anyhow!("SAS service failed to send Ctrl+Alt+Del"))
|
||||
}
|
||||
_ => {
|
||||
error!("Unexpected response from SAS service: {}", response_str);
|
||||
Err(anyhow::anyhow!("Unexpected SAS service response: {}", response_str))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the SAS service is available
|
||||
pub fn is_service_available() -> bool {
|
||||
// Try to open the pipe
|
||||
if let Ok(mut pipe) = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(PIPE_NAME)
|
||||
{
|
||||
// Send a ping command
|
||||
if pipe.write_all(b"ping\n").is_ok() {
|
||||
let mut response = [0u8; 64];
|
||||
if let Ok(n) = pipe.read(&mut response) {
|
||||
let response_str = String::from_utf8_lossy(&response[..n]);
|
||||
return response_str.trim() == "pong";
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Get information about SAS service status
|
||||
pub fn get_service_status() -> String {
|
||||
if is_service_available() {
|
||||
"SAS service is running and responding".to_string()
|
||||
} else {
|
||||
"SAS service is not available".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_service_check() {
|
||||
// This test just checks the function runs without panicking
|
||||
let available = is_service_available();
|
||||
println!("SAS service available: {}", available);
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,93 @@ pub fn uninstall() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install the SAS service if the binary is available
|
||||
/// This allows the agent to send Ctrl+Alt+Del even without SYSTEM privileges
|
||||
#[cfg(windows)]
|
||||
pub fn install_sas_service() -> Result<()> {
|
||||
info!("Attempting to install SAS service...");
|
||||
|
||||
// Check if the SAS service binary exists alongside the agent
|
||||
let exe_path = std::env::current_exe()?;
|
||||
let exe_dir = exe_path.parent().ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
|
||||
let sas_binary = exe_dir.join("guruconnect-sas-service.exe");
|
||||
|
||||
if !sas_binary.exists() {
|
||||
// Also check in Program Files
|
||||
let program_files = std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe");
|
||||
if !program_files.exists() {
|
||||
warn!("SAS service binary not found");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Run the install command
|
||||
let sas_path = if sas_binary.exists() {
|
||||
sas_binary
|
||||
} else {
|
||||
std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")
|
||||
};
|
||||
|
||||
let output = std::process::Command::new(&sas_path)
|
||||
.arg("install")
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
if result.status.success() {
|
||||
info!("SAS service installed successfully");
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
warn!("SAS service install failed: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to run SAS service installer: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uninstall the SAS service
|
||||
#[cfg(windows)]
|
||||
pub fn uninstall_sas_service() -> Result<()> {
|
||||
info!("Attempting to uninstall SAS service...");
|
||||
|
||||
// Try to find and run the uninstall command
|
||||
let paths = [
|
||||
std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.join("guruconnect-sas-service.exe"))),
|
||||
Some(std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")),
|
||||
];
|
||||
|
||||
for path_opt in paths.iter() {
|
||||
if let Some(ref path) = path_opt {
|
||||
if path.exists() {
|
||||
let output = std::process::Command::new(path)
|
||||
.arg("uninstall")
|
||||
.output();
|
||||
|
||||
if let Ok(result) = output {
|
||||
if result.status.success() {
|
||||
info!("SAS service uninstalled successfully");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("SAS service binary not found for uninstall");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the SAS service is installed and running
|
||||
#[cfg(windows)]
|
||||
pub fn check_sas_service() -> bool {
|
||||
use crate::sas_client;
|
||||
sas_client::is_service_available()
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn add_to_startup() -> Result<()> {
|
||||
warn!("Startup persistence not implemented for this platform");
|
||||
@@ -193,3 +280,19 @@ pub fn uninstall() -> Result<()> {
|
||||
warn!("Uninstall not implemented for this platform");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn install_sas_service() -> Result<()> {
|
||||
warn!("SAS service only available on Windows");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn uninstall_sas_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn check_sas_service() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user