Files
claudetools/projects/msp-tools/guru-connect/agent/src/install.rs
Mike Swanson 6c316aa701 Add VPN configuration tools and agent documentation
Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection
and enhanced agent documentation framework.

VPN Configuration (PST-NW-VPN):
- Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS
- Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24)
- Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment
- Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2
- Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic
- Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes)
- Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper
- vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts
- OpenVPN config files (Windows-compatible, abandoned for L2TP)

Key VPN Implementation Details:
- L2TP creates PPP adapter with connection name as interface description
- UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24
- Split-tunnel enabled (only remote traffic through VPN)
- All-user connection for pre-login auto-connect via scheduled task
- Authentication: CHAP + MSChapv2 for UniFi compatibility

Agent Documentation:
- AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents
- documentation-squire.md: Documentation and task management specialist agent
- Updated all agent markdown files with standardized formatting

Project Organization:
- Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs)
- Cleaned up old session JSONL files from projects/msp-tools/
- Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows)
- Added guru-rmm server components and deployment configs

Technical Notes:
- VPN IP pool: 192.168.4.x (client gets 192.168.4.6)
- Remote network: 192.168.0.0/24 (router at 192.168.0.10)
- PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7
- Credentials: pst-admin / 24Hearts$

Files: 15 VPN scripts, 2 agent docs, conversation log reorganization,
guru-connect/guru-rmm infrastructure additions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 11:51:47 -07:00

418 lines
12 KiB
Rust

//! Installation and protocol handler registration
//!
//! Handles:
//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
//! - Protocol handler registration (guruconnect://)
//! - UAC elevation with graceful fallback
use anyhow::{anyhow, Result};
use tracing::{info, warn, error};
#[cfg(windows)]
use windows::{
core::PCWSTR,
Win32::Foundation::HANDLE,
Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
Win32::System::Registry::{
RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
},
Win32::UI::Shell::ShellExecuteW,
Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
};
#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
/// Install locations
pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%
/// Check if running with elevated privileges
#[cfg(windows)]
pub fn is_elevated() -> bool {
unsafe {
let mut token_handle = HANDLE::default();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
return false;
}
let mut elevation = TOKEN_ELEVATION::default();
let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
let result = GetTokenInformation(
token_handle,
TokenElevation,
Some(&mut elevation as *mut _ as *mut _),
size,
&mut size,
);
let _ = windows::Win32::Foundation::CloseHandle(token_handle);
result.is_ok() && elevation.TokenIsElevated != 0
}
}
#[cfg(not(windows))]
pub fn is_elevated() -> bool {
unsafe { libc::geteuid() == 0 }
}
/// Get the install path based on elevation status
pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
if elevated {
std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
} else {
let local_app_data = std::env::var("LOCALAPPDATA")
.unwrap_or_else(|_| {
let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
format!(r"{}\AppData\Local", home)
});
std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
}
}
/// Get the executable path
pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
install_path.join("guruconnect.exe")
}
/// Attempt to elevate and re-run with install command
#[cfg(windows)]
pub fn try_elevate_and_install() -> Result<bool> {
let exe_path = std::env::current_exe()?;
let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
.encode_wide()
.chain(std::iter::once(0))
.collect();
let verb: Vec<u16> = OsStr::new("runas")
.encode_wide()
.chain(std::iter::once(0))
.collect();
let params: Vec<u16> = OsStr::new("install --elevated")
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
let result = ShellExecuteW(
None,
PCWSTR(verb.as_ptr()),
PCWSTR(exe_path_wide.as_ptr()),
PCWSTR(params.as_ptr()),
PCWSTR::null(),
SW_SHOWNORMAL,
);
// ShellExecuteW returns > 32 on success
if result.0 as usize > 32 {
info!("UAC elevation requested");
Ok(true)
} else {
warn!("UAC elevation denied or failed");
Ok(false)
}
}
}
#[cfg(not(windows))]
pub fn try_elevate_and_install() -> Result<bool> {
Ok(false)
}
/// Register the guruconnect:// protocol handler
#[cfg(windows)]
pub fn register_protocol_handler(elevated: bool) -> Result<()> {
let install_path = get_install_path(elevated);
let exe_path = get_exe_path(&install_path);
let exe_path_str = exe_path.to_string_lossy();
// Command to execute: "C:\...\guruconnect.exe" "launch" "%1"
let command = format!("\"{}\" launch \"%1\"", exe_path_str);
// Choose registry root based on elevation
let root_key = if elevated {
HKEY_CLASSES_ROOT
} else {
// User-level registration under Software\Classes
HKEY_CURRENT_USER
};
let base_path = if elevated {
"guruconnect"
} else {
r"Software\Classes\guruconnect"
};
unsafe {
// Create guruconnect key
let mut protocol_key = HKEY::default();
let key_path = to_wide(base_path);
let result = RegCreateKeyExW(
root_key,
PCWSTR(key_path.as_ptr()),
0,
PCWSTR::null(),
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
None,
&mut protocol_key,
None,
);
if result.is_err() {
return Err(anyhow!("Failed to create protocol key: {:?}", result));
}
// Set default value (protocol description)
let description = to_wide("GuruConnect Protocol");
let result = RegSetValueExW(
protocol_key,
PCWSTR::null(),
0,
REG_SZ,
Some(&description_to_bytes(&description)),
);
if result.is_err() {
let _ = RegCloseKey(protocol_key);
return Err(anyhow!("Failed to set protocol description: {:?}", result));
}
// Set URL Protocol (empty string indicates this is a protocol handler)
let url_protocol = to_wide("URL Protocol");
let empty = to_wide("");
let result = RegSetValueExW(
protocol_key,
PCWSTR(url_protocol.as_ptr()),
0,
REG_SZ,
Some(&description_to_bytes(&empty)),
);
if result.is_err() {
let _ = RegCloseKey(protocol_key);
return Err(anyhow!("Failed to set URL Protocol: {:?}", result));
}
let _ = RegCloseKey(protocol_key);
// Create shell\open\command key
let command_path = if elevated {
r"guruconnect\shell\open\command"
} else {
r"Software\Classes\guruconnect\shell\open\command"
};
let command_key_path = to_wide(command_path);
let mut command_key = HKEY::default();
let result = RegCreateKeyExW(
root_key,
PCWSTR(command_key_path.as_ptr()),
0,
PCWSTR::null(),
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
None,
&mut command_key,
None,
);
if result.is_err() {
return Err(anyhow!("Failed to create command key: {:?}", result));
}
// Set the command
let command_wide = to_wide(&command);
let result = RegSetValueExW(
command_key,
PCWSTR::null(),
0,
REG_SZ,
Some(&description_to_bytes(&command_wide)),
);
if result.is_err() {
let _ = RegCloseKey(command_key);
return Err(anyhow!("Failed to set command: {:?}", result));
}
let _ = RegCloseKey(command_key);
}
info!("Protocol handler registered: guruconnect://");
Ok(())
}
#[cfg(not(windows))]
pub fn register_protocol_handler(_elevated: bool) -> Result<()> {
warn!("Protocol handler registration not supported on this platform");
Ok(())
}
/// Install the application
pub fn install(force_user_install: bool) -> Result<()> {
let elevated = is_elevated();
// If not elevated and not forcing user install, try to elevate
if !elevated && !force_user_install {
info!("Attempting UAC elevation for system-wide install...");
match try_elevate_and_install() {
Ok(true) => {
// Elevation was requested, exit this instance
// The elevated instance will continue the install
info!("Elevated process started, exiting current instance");
std::process::exit(0);
}
Ok(false) => {
info!("UAC denied, falling back to user install");
}
Err(e) => {
warn!("Elevation failed: {}, falling back to user install", e);
}
}
}
let install_path = get_install_path(elevated);
let exe_path = get_exe_path(&install_path);
info!("Installing to: {}", install_path.display());
// Create install directory
std::fs::create_dir_all(&install_path)?;
// Copy ourselves to install location
let current_exe = std::env::current_exe()?;
if current_exe != exe_path {
std::fs::copy(&current_exe, &exe_path)?;
info!("Copied executable to: {}", exe_path.display());
}
// Register protocol handler
register_protocol_handler(elevated)?;
info!("Installation complete!");
if elevated {
info!("Installed system-wide to: {}", install_path.display());
} else {
info!("Installed for current user to: {}", install_path.display());
}
Ok(())
}
/// Check if the guruconnect:// protocol handler is registered
#[cfg(windows)]
pub fn is_protocol_handler_registered() -> bool {
use windows::Win32::System::Registry::{
RegOpenKeyExW, RegCloseKey, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
};
unsafe {
// Check system-wide registration (HKCR\guruconnect)
let mut key = HKEY::default();
let key_path = to_wide("guruconnect");
if RegOpenKeyExW(
HKEY_CLASSES_ROOT,
PCWSTR(key_path.as_ptr()),
0,
KEY_READ,
&mut key,
).is_ok() {
let _ = RegCloseKey(key);
return true;
}
// Check user-level registration (HKCU\Software\Classes\guruconnect)
let key_path = to_wide(r"Software\Classes\guruconnect");
if RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR(key_path.as_ptr()),
0,
KEY_READ,
&mut key,
).is_ok() {
let _ = RegCloseKey(key);
return true;
}
}
false
}
#[cfg(not(windows))]
pub fn is_protocol_handler_registered() -> bool {
// On non-Windows, assume not registered (or check ~/.local/share/applications)
false
}
/// Parse a guruconnect:// URL and extract session parameters
pub fn parse_protocol_url(url_str: &str) -> Result<(String, String, Option<String>)> {
// Expected formats:
// guruconnect://view/SESSION_ID
// guruconnect://view/SESSION_ID?token=API_KEY
// guruconnect://connect/SESSION_ID?server=wss://...&token=API_KEY
//
// Note: In URL parsing, "view" becomes the host, SESSION_ID is the path
let url = url::Url::parse(url_str)
.map_err(|e| anyhow!("Invalid URL: {}", e))?;
if url.scheme() != "guruconnect" {
return Err(anyhow!("Invalid scheme: expected guruconnect://"));
}
// The "action" (view/connect) is parsed as the host
let action = url.host_str()
.ok_or_else(|| anyhow!("Missing action in URL"))?;
// The session ID is the first path segment
let path = url.path().trim_start_matches('/');
info!("URL path: '{}', host: '{:?}'", path, url.host_str());
let session_id = if path.is_empty() {
return Err(anyhow!("Invalid URL: Missing session ID (path was empty, full URL: {})", url_str));
} else {
path.split('/').next().unwrap_or("").to_string()
};
if session_id.is_empty() {
return Err(anyhow!("Missing session ID"));
}
// Extract query parameters
let mut server = None;
let mut token = None;
for (key, value) in url.query_pairs() {
match key.as_ref() {
"server" => server = Some(value.to_string()),
"token" | "api_key" => token = Some(value.to_string()),
_ => {}
}
}
// Default server if not specified
let server = server.unwrap_or_else(|| "wss://connect.azcomputerguru.com/ws/viewer".to_string());
match action {
"view" | "connect" => Ok((server, session_id, token)),
_ => Err(anyhow!("Unknown action: {}", action)),
}
}
// Helper functions for Windows registry operations
#[cfg(windows)]
fn to_wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
#[cfg(windows)]
fn description_to_bytes(wide: &[u16]) -> Vec<u8> {
wide.iter()
.flat_map(|w| w.to_le_bytes())
.collect()
}