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>
This commit is contained in:
459
projects/msp-tools/guru-connect/agent/src/config.rs
Normal file
459
projects/msp-tools/guru-connect/agent/src/config.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
//! Agent configuration management
|
||||
//!
|
||||
//! Supports three configuration sources (in priority order):
|
||||
//! 1. Embedded config (magic bytes appended to executable)
|
||||
//! 2. Config file (guruconnect.toml or %ProgramData%\GuruConnect\agent.toml)
|
||||
//! 3. Environment variables (fallback)
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Magic marker for embedded configuration (10 bytes)
|
||||
const MAGIC_MARKER: &[u8] = b"GURUCONFIG";
|
||||
|
||||
/// Embedded configuration data (appended to executable)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmbeddedConfig {
|
||||
/// Server WebSocket URL
|
||||
pub server_url: String,
|
||||
/// API key for authentication
|
||||
pub api_key: String,
|
||||
/// Company/organization name
|
||||
#[serde(default)]
|
||||
pub company: Option<String>,
|
||||
/// Site/location name
|
||||
#[serde(default)]
|
||||
pub site: Option<String>,
|
||||
/// Tags for categorization
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// Detected run mode based on filename
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RunMode {
|
||||
/// Viewer-only installation (filename contains "Viewer")
|
||||
Viewer,
|
||||
/// Temporary support session (filename contains 6-digit code)
|
||||
TempSupport(String),
|
||||
/// Permanent agent with embedded config
|
||||
PermanentAgent,
|
||||
/// Unknown/default mode
|
||||
Default,
|
||||
}
|
||||
|
||||
/// Agent configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Server WebSocket URL (e.g., wss://connect.example.com/ws)
|
||||
pub server_url: String,
|
||||
|
||||
/// Agent API key for authentication
|
||||
pub api_key: String,
|
||||
|
||||
/// Unique agent identifier (generated on first run)
|
||||
#[serde(default = "generate_agent_id")]
|
||||
pub agent_id: String,
|
||||
|
||||
/// Optional hostname override
|
||||
pub hostname_override: Option<String>,
|
||||
|
||||
/// Company/organization name (from embedded config)
|
||||
#[serde(default)]
|
||||
pub company: Option<String>,
|
||||
|
||||
/// Site/location name (from embedded config)
|
||||
#[serde(default)]
|
||||
pub site: Option<String>,
|
||||
|
||||
/// Tags for categorization (from embedded config)
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Support code for one-time support sessions (set via command line or filename)
|
||||
#[serde(skip)]
|
||||
pub support_code: Option<String>,
|
||||
|
||||
/// Capture settings
|
||||
#[serde(default)]
|
||||
pub capture: CaptureConfig,
|
||||
|
||||
/// Encoding settings
|
||||
#[serde(default)]
|
||||
pub encoding: EncodingConfig,
|
||||
}
|
||||
|
||||
fn generate_agent_id() -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CaptureConfig {
|
||||
/// Target frames per second (1-60)
|
||||
#[serde(default = "default_fps")]
|
||||
pub fps: u32,
|
||||
|
||||
/// Use DXGI Desktop Duplication (recommended)
|
||||
#[serde(default = "default_true")]
|
||||
pub use_dxgi: bool,
|
||||
|
||||
/// Fall back to GDI if DXGI fails
|
||||
#[serde(default = "default_true")]
|
||||
pub gdi_fallback: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EncodingConfig {
|
||||
/// Preferred codec (auto, raw, vp9, h264)
|
||||
#[serde(default = "default_codec")]
|
||||
pub codec: String,
|
||||
|
||||
/// Quality (1-100, higher = better quality, more bandwidth)
|
||||
#[serde(default = "default_quality")]
|
||||
pub quality: u32,
|
||||
|
||||
/// Use hardware encoding if available
|
||||
#[serde(default = "default_true")]
|
||||
pub hardware_encoding: bool,
|
||||
}
|
||||
|
||||
fn default_fps() -> u32 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_codec() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
|
||||
fn default_quality() -> u32 {
|
||||
75
|
||||
}
|
||||
|
||||
impl Default for CaptureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fps: default_fps(),
|
||||
use_dxgi: true,
|
||||
gdi_fallback: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EncodingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codec: default_codec(),
|
||||
quality: default_quality(),
|
||||
hardware_encoding: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Detect run mode from executable filename
|
||||
pub fn detect_run_mode() -> RunMode {
|
||||
let exe_path = match std::env::current_exe() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return RunMode::Default,
|
||||
};
|
||||
|
||||
let filename = match exe_path.file_stem() {
|
||||
Some(s) => s.to_string_lossy().to_string(),
|
||||
None => return RunMode::Default,
|
||||
};
|
||||
|
||||
let filename_lower = filename.to_lowercase();
|
||||
|
||||
// Check for viewer mode
|
||||
if filename_lower.contains("viewer") {
|
||||
info!("Detected viewer mode from filename: {}", filename);
|
||||
return RunMode::Viewer;
|
||||
}
|
||||
|
||||
// Check for support code in filename (6-digit number)
|
||||
if let Some(code) = Self::extract_support_code(&filename) {
|
||||
info!("Detected support code from filename: {}", code);
|
||||
return RunMode::TempSupport(code);
|
||||
}
|
||||
|
||||
// Check for embedded config
|
||||
if Self::has_embedded_config() {
|
||||
info!("Detected embedded config in executable");
|
||||
return RunMode::PermanentAgent;
|
||||
}
|
||||
|
||||
RunMode::Default
|
||||
}
|
||||
|
||||
/// Extract 6-digit support code from filename
|
||||
fn extract_support_code(filename: &str) -> Option<String> {
|
||||
// Look for patterns like "GuruConnect-123456" or "GuruConnect_123456"
|
||||
for part in filename.split(|c| c == '-' || c == '_' || c == '.') {
|
||||
let trimmed = part.trim();
|
||||
if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check if last 6 characters are all digits
|
||||
if filename.len() >= 6 {
|
||||
let last_six = &filename[filename.len() - 6..];
|
||||
if last_six.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Some(last_six.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if embedded configuration exists in the executable
|
||||
pub fn has_embedded_config() -> bool {
|
||||
Self::read_embedded_config().is_ok()
|
||||
}
|
||||
|
||||
/// Read embedded configuration from the executable
|
||||
pub fn read_embedded_config() -> Result<EmbeddedConfig> {
|
||||
let exe_path = std::env::current_exe()
|
||||
.context("Failed to get current executable path")?;
|
||||
|
||||
let mut file = std::fs::File::open(&exe_path)
|
||||
.context("Failed to open executable for reading")?;
|
||||
|
||||
let file_size = file.metadata()?.len();
|
||||
if file_size < (MAGIC_MARKER.len() + 4) as u64 {
|
||||
return Err(anyhow!("File too small to contain embedded config"));
|
||||
}
|
||||
|
||||
// Read the last part of the file to find magic marker
|
||||
// Structure: [PE binary][GURUCONFIG][length:u32][json config]
|
||||
// We need to search backwards from the end
|
||||
|
||||
// Read last 64KB (should be more than enough for config)
|
||||
let search_size = std::cmp::min(65536, file_size as usize);
|
||||
let search_start = file_size - search_size as u64;
|
||||
|
||||
file.seek(SeekFrom::Start(search_start))?;
|
||||
let mut buffer = vec![0u8; search_size];
|
||||
file.read_exact(&mut buffer)?;
|
||||
|
||||
// Find magic marker
|
||||
let marker_pos = buffer.windows(MAGIC_MARKER.len())
|
||||
.rposition(|window| window == MAGIC_MARKER)
|
||||
.ok_or_else(|| anyhow!("Magic marker not found"))?;
|
||||
|
||||
// Read config length (4 bytes after marker)
|
||||
let length_start = marker_pos + MAGIC_MARKER.len();
|
||||
if length_start + 4 > buffer.len() {
|
||||
return Err(anyhow!("Invalid embedded config: length field truncated"));
|
||||
}
|
||||
|
||||
let config_length = u32::from_le_bytes([
|
||||
buffer[length_start],
|
||||
buffer[length_start + 1],
|
||||
buffer[length_start + 2],
|
||||
buffer[length_start + 3],
|
||||
]) as usize;
|
||||
|
||||
// Read config data
|
||||
let config_start = length_start + 4;
|
||||
if config_start + config_length > buffer.len() {
|
||||
return Err(anyhow!("Invalid embedded config: data truncated"));
|
||||
}
|
||||
|
||||
let config_bytes = &buffer[config_start..config_start + config_length];
|
||||
let config: EmbeddedConfig = serde_json::from_slice(config_bytes)
|
||||
.context("Failed to parse embedded config JSON")?;
|
||||
|
||||
info!("Loaded embedded config: server={}, company={:?}",
|
||||
config.server_url, config.company);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Check if an explicit agent configuration file exists
|
||||
/// This returns true only if there's a real config file, not generated defaults
|
||||
pub fn has_agent_config() -> bool {
|
||||
// Check for embedded config first
|
||||
if Self::has_embedded_config() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for config in current directory
|
||||
let local_config = PathBuf::from("guruconnect.toml");
|
||||
if local_config.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check in program data directory (Windows)
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(program_data) = std::env::var("ProgramData") {
|
||||
let path = PathBuf::from(program_data)
|
||||
.join("GuruConnect")
|
||||
.join("agent.toml");
|
||||
if path.exists() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Load configuration from embedded config, file, or environment
|
||||
pub fn load() -> Result<Self> {
|
||||
// Priority 1: Try loading from embedded config
|
||||
if let Ok(embedded) = Self::read_embedded_config() {
|
||||
info!("Using embedded configuration");
|
||||
let config = Config {
|
||||
server_url: embedded.server_url,
|
||||
api_key: embedded.api_key,
|
||||
agent_id: generate_agent_id(),
|
||||
hostname_override: None,
|
||||
company: embedded.company,
|
||||
site: embedded.site,
|
||||
tags: embedded.tags,
|
||||
support_code: None,
|
||||
capture: CaptureConfig::default(),
|
||||
encoding: EncodingConfig::default(),
|
||||
};
|
||||
|
||||
// Save to file for persistence (so agent_id is preserved)
|
||||
let _ = config.save();
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// Priority 2: Try loading from config file
|
||||
let config_path = Self::config_path();
|
||||
|
||||
if config_path.exists() {
|
||||
let contents = std::fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read config from {:?}", config_path))?;
|
||||
|
||||
let mut config: Config = toml::from_str(&contents)
|
||||
.with_context(|| "Failed to parse config file")?;
|
||||
|
||||
// Ensure agent_id is set and saved
|
||||
if config.agent_id.is_empty() {
|
||||
config.agent_id = generate_agent_id();
|
||||
let _ = config.save();
|
||||
}
|
||||
|
||||
// support_code is always None when loading from file (set via CLI)
|
||||
config.support_code = None;
|
||||
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// Priority 3: Fall back to environment variables
|
||||
let server_url = std::env::var("GURUCONNECT_SERVER_URL")
|
||||
.unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());
|
||||
|
||||
let api_key = std::env::var("GURUCONNECT_API_KEY")
|
||||
.unwrap_or_else(|_| "dev-key".to_string());
|
||||
|
||||
let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
|
||||
.unwrap_or_else(|_| generate_agent_id());
|
||||
|
||||
let config = Config {
|
||||
server_url,
|
||||
api_key,
|
||||
agent_id,
|
||||
hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
|
||||
company: None,
|
||||
site: None,
|
||||
tags: Vec::new(),
|
||||
support_code: None,
|
||||
capture: CaptureConfig::default(),
|
||||
encoding: EncodingConfig::default(),
|
||||
};
|
||||
|
||||
// Save config with generated agent_id for persistence
|
||||
let _ = config.save();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Get the configuration file path
|
||||
fn config_path() -> PathBuf {
|
||||
// Check for config in current directory first
|
||||
let local_config = PathBuf::from("guruconnect.toml");
|
||||
if local_config.exists() {
|
||||
return local_config;
|
||||
}
|
||||
|
||||
// Check in program data directory (Windows)
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(program_data) = std::env::var("ProgramData") {
|
||||
let path = PathBuf::from(program_data)
|
||||
.join("GuruConnect")
|
||||
.join("agent.toml");
|
||||
if path.exists() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to local config
|
||||
local_config
|
||||
}
|
||||
|
||||
/// Get the hostname to use
|
||||
pub fn hostname(&self) -> String {
|
||||
self.hostname_override
|
||||
.clone()
|
||||
.unwrap_or_else(|| {
|
||||
hostname::get()
|
||||
.map(|h| h.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Save current configuration to file
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config_path = Self::config_path();
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let contents = toml::to_string_pretty(self)?;
|
||||
std::fs::write(&config_path, contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Example configuration file content
|
||||
pub fn example_config() -> &'static str {
|
||||
r#"# GuruConnect Agent Configuration
|
||||
|
||||
# Server connection
|
||||
server_url = "wss://connect.example.com/ws"
|
||||
api_key = "your-agent-api-key"
|
||||
agent_id = "auto-generated-uuid"
|
||||
|
||||
# Optional: override hostname
|
||||
# hostname_override = "custom-hostname"
|
||||
|
||||
[capture]
|
||||
fps = 30
|
||||
use_dxgi = true
|
||||
gdi_fallback = true
|
||||
|
||||
[encoding]
|
||||
codec = "auto" # auto, raw, vp9, h264
|
||||
quality = 75 # 1-100
|
||||
hardware_encoding = true
|
||||
"#
|
||||
}
|
||||
Reference in New Issue
Block a user