Files
guru-connect/agent/src/bin/sas_service.rs
Mike Swanson 598a6737de Fix SAS Service build errors
- Use raw FFI for named pipe operations instead of windows crate APIs
- Add Win32_System_IO feature to Cargo.toml
- Define pipe constants manually to avoid missing exports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 20:55:36 -07:00

639 lines
20 KiB
Rust

//! 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 as IoWrite};
use std::sync::mpsc;
use std::time::Duration;
use anyhow::{Context, Result};
use windows::core::{s, w};
use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
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";
// Windows named pipe constants
const PIPE_ACCESS_DUPLEX: u32 = 0x00000003;
const PIPE_TYPE_MESSAGE: u32 = 0x00000004;
const PIPE_READMODE_MESSAGE: u32 = 0x00000002;
const PIPE_WAIT: u32 = 0x00000000;
const PIPE_UNLIMITED_INSTANCES: u32 = 255;
const INVALID_HANDLE_VALUE: isize = -1;
const SECURITY_DESCRIPTOR_REVISION: u32 = 1;
// FFI declarations for named pipe operations
#[link(name = "kernel32")]
extern "system" {
fn CreateNamedPipeW(
lpName: *const u16,
dwOpenMode: u32,
dwPipeMode: u32,
nMaxInstances: u32,
nOutBufferSize: u32,
nInBufferSize: u32,
nDefaultTimeOut: u32,
lpSecurityAttributes: *mut SECURITY_ATTRIBUTES,
) -> isize;
fn ConnectNamedPipe(hNamedPipe: isize, lpOverlapped: *mut std::ffi::c_void) -> i32;
fn DisconnectNamedPipe(hNamedPipe: isize) -> i32;
fn CloseHandle(hObject: isize) -> i32;
fn ReadFile(
hFile: isize,
lpBuffer: *mut u8,
nNumberOfBytesToRead: u32,
lpNumberOfBytesRead: *mut u32,
lpOverlapped: *mut std::ffi::c_void,
) -> i32;
fn WriteFile(
hFile: isize,
lpBuffer: *const u8,
nNumberOfBytesToWrite: u32,
lpNumberOfBytesWritten: *mut u32,
lpOverlapped: *mut std::ffi::c_void,
) -> i32;
fn FlushFileBuffers(hFile: isize) -> i32;
}
#[link(name = "advapi32")]
extern "system" {
fn InitializeSecurityDescriptor(pSecurityDescriptor: *mut u8, dwRevision: u32) -> i32;
fn SetSecurityDescriptorDacl(
pSecurityDescriptor: *mut u8,
bDaclPresent: i32,
pDacl: *mut std::ffi::c_void,
bDaclDefaulted: i32,
) -> i32;
}
#[repr(C)]
struct SECURITY_ATTRIBUTES {
nLength: u32,
lpSecurityDescriptor: *mut u8,
bInheritHandle: i32,
}
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 security descriptor that allows everyone
let mut sd = [0u8; 256];
unsafe {
if InitializeSecurityDescriptor(sd.as_mut_ptr(), SECURITY_DESCRIPTOR_REVISION) == 0 {
tracing::error!("Failed to initialize security descriptor");
std::thread::sleep(Duration::from_secs(1));
continue;
}
// Set NULL DACL = allow everyone
if SetSecurityDescriptorDacl(sd.as_mut_ptr(), 1, std::ptr::null_mut(), 0) == 0 {
tracing::error!("Failed to set security descriptor DACL");
std::thread::sleep(Duration::from_secs(1));
continue;
}
}
let mut sa = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: sd.as_mut_ptr(),
bInheritHandle: 0,
};
// Create the pipe name as wide string
let pipe_name: Vec<u16> = PIPE_NAME.encode_utf16().chain(std::iter::once(0)).collect();
// Create the named pipe
let pipe = unsafe {
CreateNamedPipeW(
pipe_name.as_ptr(),
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
512,
512,
0,
&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, std::ptr::null_mut()) };
if connected == 0 {
let err = std::io::Error::last_os_error();
// ERROR_PIPE_CONNECTED (535) means client connected between Create and Connect
if err.raw_os_error() != Some(535) {
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,
buffer.as_mut_ptr(),
buffer.len() as u32,
&mut bytes_read,
std::ptr::null_mut(),
)
};
if read_result != 0 && 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;
unsafe {
WriteFile(
pipe,
response.as_ptr(),
response.len() as u32,
&mut bytes_written,
std::ptr::null_mut(),
);
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(&current_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(())
}