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:
2026-01-18 11:51:47 -07:00
parent b0a68d89bf
commit 6c316aa701
272 changed files with 37068 additions and 2 deletions

View File

@@ -0,0 +1,159 @@
//! Display enumeration and information
use anyhow::Result;
/// Information about a display/monitor
#[derive(Debug, Clone)]
pub struct Display {
/// Unique display ID
pub id: u32,
/// Display name (e.g., "\\\\.\\DISPLAY1")
pub name: String,
/// X position in virtual screen coordinates
pub x: i32,
/// Y position in virtual screen coordinates
pub y: i32,
/// Width in pixels
pub width: u32,
/// Height in pixels
pub height: u32,
/// Whether this is the primary display
pub is_primary: bool,
/// Platform-specific handle (HMONITOR on Windows)
#[cfg(windows)]
pub handle: isize,
}
/// Display info for protocol messages
#[derive(Debug, Clone)]
pub struct DisplayInfo {
pub displays: Vec<Display>,
pub primary_id: u32,
}
impl Display {
/// Total pixels in the display
pub fn pixel_count(&self) -> u32 {
self.width * self.height
}
/// Bytes needed for BGRA frame buffer
pub fn buffer_size(&self) -> usize {
(self.width * self.height * 4) as usize
}
}
/// Enumerate all connected displays
#[cfg(windows)]
pub fn enumerate_displays() -> Result<Vec<Display>> {
use windows::Win32::Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITORINFOEXW,
};
use windows::Win32::Foundation::{BOOL, LPARAM, RECT};
use std::mem;
let mut displays = Vec::new();
let mut display_id = 0u32;
// Callback for EnumDisplayMonitors
unsafe extern "system" fn enum_callback(
hmonitor: HMONITOR,
_hdc: windows::Win32::Graphics::Gdi::HDC,
_rect: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let displays = &mut *(lparam.0 as *mut Vec<(HMONITOR, u32)>);
let id = displays.len() as u32;
displays.push((hmonitor, id));
BOOL(1) // Continue enumeration
}
// Collect all monitor handles
let mut monitors: Vec<(windows::Win32::Graphics::Gdi::HMONITOR, u32)> = Vec::new();
unsafe {
let result = EnumDisplayMonitors(
None,
None,
Some(enum_callback),
LPARAM(&mut monitors as *mut _ as isize),
);
if !result.as_bool() {
anyhow::bail!("EnumDisplayMonitors failed");
}
}
// Get detailed info for each monitor
for (hmonitor, id) in monitors {
let mut info: MONITORINFOEXW = unsafe { mem::zeroed() };
info.monitorInfo.cbSize = mem::size_of::<MONITORINFOEXW>() as u32;
unsafe {
if GetMonitorInfoW(hmonitor, &mut info.monitorInfo as *mut _ as *mut _).as_bool() {
let rect = info.monitorInfo.rcMonitor;
let name = String::from_utf16_lossy(
&info.szDevice[..info.szDevice.iter().position(|&c| c == 0).unwrap_or(info.szDevice.len())]
);
let is_primary = (info.monitorInfo.dwFlags & 1) != 0; // MONITORINFOF_PRIMARY
displays.push(Display {
id,
name,
x: rect.left,
y: rect.top,
width: (rect.right - rect.left) as u32,
height: (rect.bottom - rect.top) as u32,
is_primary,
handle: hmonitor.0 as isize,
});
}
}
}
// Sort by position (left to right, top to bottom)
displays.sort_by(|a, b| {
if a.y != b.y {
a.y.cmp(&b.y)
} else {
a.x.cmp(&b.x)
}
});
// Reassign IDs after sorting
for (i, display) in displays.iter_mut().enumerate() {
display.id = i as u32;
}
if displays.is_empty() {
anyhow::bail!("No displays found");
}
Ok(displays)
}
#[cfg(not(windows))]
pub fn enumerate_displays() -> Result<Vec<Display>> {
anyhow::bail!("Display enumeration only supported on Windows")
}
/// Get display info for protocol
pub fn get_display_info() -> Result<DisplayInfo> {
let displays = enumerate_displays()?;
let primary_id = displays
.iter()
.find(|d| d.is_primary)
.map(|d| d.id)
.unwrap_or(0);
Ok(DisplayInfo {
displays,
primary_id,
})
}

View File

@@ -0,0 +1,326 @@
//! DXGI Desktop Duplication screen capture
//!
//! Uses the Windows Desktop Duplication API (available on Windows 8+) for
//! high-performance, low-latency screen capture with hardware acceleration.
//!
//! Reference: RustDesk's scrap library implementation
use super::{CapturedFrame, Capturer, DirtyRect, Display};
use anyhow::{Context, Result};
use std::ptr;
use std::time::Instant;
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN;
use windows::Win32::Graphics::Direct3D11::{
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
D3D11_USAGE_STAGING, D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ,
};
use windows::Win32::Graphics::Dxgi::{
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIOutput, IDXGIOutput1,
IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST,
DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
DXGI_RESOURCE_PRIORITY_MAXIMUM,
};
use windows::core::Interface;
/// DXGI Desktop Duplication capturer
pub struct DxgiCapturer {
display: Display,
device: ID3D11Device,
context: ID3D11DeviceContext,
duplication: IDXGIOutputDuplication,
staging_texture: Option<ID3D11Texture2D>,
width: u32,
height: u32,
last_frame: Option<Vec<u8>>,
}
impl DxgiCapturer {
/// Create a new DXGI capturer for the specified display
pub fn new(display: Display) -> Result<Self> {
let (device, context, duplication, desc) = Self::create_duplication(&display)?;
Ok(Self {
display,
device,
context,
duplication,
staging_texture: None,
width: desc.ModeDesc.Width,
height: desc.ModeDesc.Height,
last_frame: None,
})
}
/// Create D3D device and output duplication
fn create_duplication(
target_display: &Display,
) -> Result<(ID3D11Device, ID3D11DeviceContext, IDXGIOutputDuplication, DXGI_OUTDUPL_DESC)> {
unsafe {
// Create DXGI factory
let factory: IDXGIFactory1 = CreateDXGIFactory1()
.context("Failed to create DXGI factory")?;
// Find the adapter and output for this display
let (adapter, output) = Self::find_adapter_output(&factory, target_display)?;
// Create D3D11 device
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
D3D11CreateDevice(
&adapter,
D3D_DRIVER_TYPE_UNKNOWN,
None,
Default::default(),
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
.context("Failed to create D3D11 device")?;
let device = device.context("D3D11 device is None")?;
let context = context.context("D3D11 context is None")?;
// Get IDXGIOutput1 interface
let output1: IDXGIOutput1 = output.cast()
.context("Failed to get IDXGIOutput1 interface")?;
// Create output duplication
let duplication = output1.DuplicateOutput(&device)
.context("Failed to create output duplication")?;
// Get duplication description
let desc = duplication.GetDesc();
tracing::info!(
"Created DXGI duplication: {}x{}, display: {}",
desc.ModeDesc.Width,
desc.ModeDesc.Height,
target_display.name
);
Ok((device, context, duplication, desc))
}
}
/// Find the adapter and output for the specified display
fn find_adapter_output(
factory: &IDXGIFactory1,
display: &Display,
) -> Result<(IDXGIAdapter1, IDXGIOutput)> {
unsafe {
let mut adapter_idx = 0u32;
loop {
// Enumerate adapters
let adapter: IDXGIAdapter1 = match factory.EnumAdapters1(adapter_idx) {
Ok(a) => a,
Err(_) => break,
};
let mut output_idx = 0u32;
loop {
// Enumerate outputs for this adapter
let output: IDXGIOutput = match adapter.EnumOutputs(output_idx) {
Ok(o) => o,
Err(_) => break,
};
// Check if this is the display we want
let desc = output.GetDesc()?;
let name = String::from_utf16_lossy(
&desc.DeviceName[..desc.DeviceName.iter().position(|&c| c == 0).unwrap_or(desc.DeviceName.len())]
);
if name == display.name || desc.Monitor.0 as isize == display.handle {
return Ok((adapter, output));
}
output_idx += 1;
}
adapter_idx += 1;
}
// If we didn't find the specific display, use the first one
let adapter: IDXGIAdapter1 = factory.EnumAdapters1(0)
.context("No adapters found")?;
let output: IDXGIOutput = adapter.EnumOutputs(0)
.context("No outputs found")?;
Ok((adapter, output))
}
}
/// Create or get the staging texture for CPU access
fn get_staging_texture(&mut self, src_texture: &ID3D11Texture2D) -> Result<&ID3D11Texture2D> {
if self.staging_texture.is_none() {
unsafe {
let mut desc = D3D11_TEXTURE2D_DESC::default();
src_texture.GetDesc(&mut desc);
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = Default::default();
desc.CPUAccessFlags = 0x20000; // D3D11_CPU_ACCESS_READ
desc.MiscFlags = Default::default();
let mut staging: Option<ID3D11Texture2D> = None;
self.device.CreateTexture2D(&desc, None, Some(&mut staging))
.context("Failed to create staging texture")?;
let staging = staging.context("Staging texture is None")?;
// Set high priority
let resource: IDXGIResource = staging.cast()?;
resource.SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM)?;
self.staging_texture = Some(staging);
}
}
Ok(self.staging_texture.as_ref().unwrap())
}
/// Acquire the next frame from the desktop
fn acquire_frame(&mut self, timeout_ms: u32) -> Result<Option<(ID3D11Texture2D, DXGI_OUTDUPL_FRAME_INFO)>> {
unsafe {
let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
let mut desktop_resource: Option<IDXGIResource> = None;
let result = self.duplication.AcquireNextFrame(
timeout_ms,
&mut frame_info,
&mut desktop_resource,
);
match result {
Ok(_) => {
let resource = desktop_resource.context("Desktop resource is None")?;
// Check if there's actually a new frame
if frame_info.LastPresentTime == 0 {
self.duplication.ReleaseFrame().ok();
return Ok(None);
}
let texture: ID3D11Texture2D = resource.cast()
.context("Failed to cast to ID3D11Texture2D")?;
Ok(Some((texture, frame_info)))
}
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
// No new frame available
Ok(None)
}
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
// Desktop duplication was invalidated, need to recreate
tracing::warn!("Desktop duplication access lost, will need to recreate");
Err(anyhow::anyhow!("Access lost"))
}
Err(e) => {
Err(e).context("Failed to acquire frame")
}
}
}
}
/// Copy frame data to CPU-accessible memory
fn copy_frame_data(&mut self, texture: &ID3D11Texture2D) -> Result<Vec<u8>> {
unsafe {
// Get or create staging texture
let staging = self.get_staging_texture(texture)?.clone();
// Copy from GPU texture to staging texture
self.context.CopyResource(&staging, texture);
// Map the staging texture for CPU read
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
self.context
.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
.context("Failed to map staging texture")?;
// Copy pixel data
let src_pitch = mapped.RowPitch as usize;
let dst_pitch = (self.width * 4) as usize;
let height = self.height as usize;
let mut data = vec![0u8; dst_pitch * height];
let src_ptr = mapped.pData as *const u8;
for y in 0..height {
let src_row = src_ptr.add(y * src_pitch);
let dst_row = data.as_mut_ptr().add(y * dst_pitch);
ptr::copy_nonoverlapping(src_row, dst_row, dst_pitch);
}
// Unmap
self.context.Unmap(&staging, 0);
Ok(data)
}
}
/// Extract dirty rectangles from frame info
fn extract_dirty_rects(&self, _frame_info: &DXGI_OUTDUPL_FRAME_INFO) -> Option<Vec<DirtyRect>> {
// TODO: Implement dirty rectangle extraction using
// IDXGIOutputDuplication::GetFrameDirtyRects and GetFrameMoveRects
// For now, return None to indicate full frame update
None
}
}
impl Capturer for DxgiCapturer {
fn capture(&mut self) -> Result<Option<CapturedFrame>> {
// Try to acquire a frame with 100ms timeout
let frame_result = self.acquire_frame(100)?;
let (texture, frame_info) = match frame_result {
Some((t, f)) => (t, f),
None => return Ok(None), // No new frame
};
// Copy frame data to CPU memory
let data = self.copy_frame_data(&texture)?;
// Release the frame
unsafe {
self.duplication.ReleaseFrame().ok();
}
// Extract dirty rectangles if available
let dirty_rects = self.extract_dirty_rects(&frame_info);
Ok(Some(CapturedFrame {
width: self.width,
height: self.height,
data,
timestamp: Instant::now(),
display_id: self.display.id,
dirty_rects,
}))
}
fn display(&self) -> &Display {
&self.display
}
fn is_valid(&self) -> bool {
// Could check if duplication is still valid
true
}
}
impl Drop for DxgiCapturer {
fn drop(&mut self) {
// Release any held frame
unsafe {
self.duplication.ReleaseFrame().ok();
}
}
}

View File

@@ -0,0 +1,148 @@
//! GDI screen capture fallback
//!
//! Uses Windows GDI (Graphics Device Interface) for screen capture.
//! Slower than DXGI but works on older systems and edge cases.
use super::{CapturedFrame, Capturer, Display};
use anyhow::Result;
use std::time::Instant;
use windows::Win32::Graphics::Gdi::{
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject,
GetDIBits, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS,
SRCCOPY, GetDC, ReleaseDC,
};
use windows::Win32::Foundation::HWND;
/// GDI-based screen capturer
pub struct GdiCapturer {
display: Display,
width: u32,
height: u32,
}
impl GdiCapturer {
/// Create a new GDI capturer for the specified display
pub fn new(display: Display) -> Result<Self> {
Ok(Self {
width: display.width,
height: display.height,
display,
})
}
/// Capture the screen using GDI
fn capture_gdi(&self) -> Result<Vec<u8>> {
unsafe {
// Get device context for the entire screen
let screen_dc = GetDC(HWND::default());
if screen_dc.is_invalid() {
anyhow::bail!("Failed to get screen DC");
}
// Create compatible DC and bitmap
let mem_dc = CreateCompatibleDC(screen_dc);
if mem_dc.is_invalid() {
ReleaseDC(HWND::default(), screen_dc);
anyhow::bail!("Failed to create compatible DC");
}
let bitmap = CreateCompatibleBitmap(screen_dc, self.width as i32, self.height as i32);
if bitmap.is_invalid() {
DeleteDC(mem_dc);
ReleaseDC(HWND::default(), screen_dc);
anyhow::bail!("Failed to create compatible bitmap");
}
// Select bitmap into memory DC
let old_bitmap = SelectObject(mem_dc, bitmap);
// Copy screen to memory DC
if let Err(e) = BitBlt(
mem_dc,
0,
0,
self.width as i32,
self.height as i32,
screen_dc,
self.display.x,
self.display.y,
SRCCOPY,
) {
SelectObject(mem_dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(mem_dc);
ReleaseDC(HWND::default(), screen_dc);
anyhow::bail!("BitBlt failed: {}", e);
}
// Prepare bitmap info for GetDIBits
let mut bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: self.width as i32,
biHeight: -(self.height as i32), // Negative for top-down
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
biSizeImage: 0,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
},
bmiColors: [Default::default()],
};
// Allocate buffer for pixel data
let buffer_size = (self.width * self.height * 4) as usize;
let mut data = vec![0u8; buffer_size];
// Get the bits
let lines = GetDIBits(
mem_dc,
bitmap,
0,
self.height,
Some(data.as_mut_ptr() as *mut _),
&mut bmi,
DIB_RGB_COLORS,
);
// Cleanup
SelectObject(mem_dc, old_bitmap);
DeleteObject(bitmap);
DeleteDC(mem_dc);
ReleaseDC(HWND::default(), screen_dc);
if lines == 0 {
anyhow::bail!("GetDIBits failed");
}
Ok(data)
}
}
}
impl Capturer for GdiCapturer {
fn capture(&mut self) -> Result<Option<CapturedFrame>> {
let data = self.capture_gdi()?;
Ok(Some(CapturedFrame {
width: self.width,
height: self.height,
data,
timestamp: Instant::now(),
display_id: self.display.id,
dirty_rects: None, // GDI doesn't provide dirty rects
}))
}
fn display(&self) -> &Display {
&self.display
}
fn is_valid(&self) -> bool {
true
}
}

View File

@@ -0,0 +1,102 @@
//! Screen capture module
//!
//! Provides DXGI Desktop Duplication for high-performance screen capture on Windows 8+,
//! with GDI fallback for legacy systems or edge cases.
#[cfg(windows)]
mod dxgi;
#[cfg(windows)]
mod gdi;
mod display;
pub use display::{Display, DisplayInfo};
use anyhow::Result;
use std::time::Instant;
/// Captured frame data
#[derive(Debug)]
pub struct CapturedFrame {
/// Frame width in pixels
pub width: u32,
/// Frame height in pixels
pub height: u32,
/// Raw BGRA pixel data (4 bytes per pixel)
pub data: Vec<u8>,
/// Timestamp when frame was captured
pub timestamp: Instant,
/// Display ID this frame is from
pub display_id: u32,
/// Regions that changed since last frame (if available)
pub dirty_rects: Option<Vec<DirtyRect>>,
}
/// Rectangular region that changed
#[derive(Debug, Clone, Copy)]
pub struct DirtyRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
/// Screen capturer trait
pub trait Capturer: Send {
/// Capture the next frame
///
/// Returns None if no new frame is available (screen unchanged)
fn capture(&mut self) -> Result<Option<CapturedFrame>>;
/// Get the current display info
fn display(&self) -> &Display;
/// Check if capturer is still valid (display may have changed)
fn is_valid(&self) -> bool;
}
/// Create a capturer for the specified display
#[cfg(windows)]
pub fn create_capturer(display: Display, use_dxgi: bool, gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
if use_dxgi {
match dxgi::DxgiCapturer::new(display.clone()) {
Ok(capturer) => {
tracing::info!("Using DXGI Desktop Duplication for capture");
return Ok(Box::new(capturer));
}
Err(e) => {
tracing::warn!("DXGI capture failed: {}, trying fallback", e);
if !gdi_fallback {
return Err(e);
}
}
}
}
// GDI fallback
tracing::info!("Using GDI for capture");
Ok(Box::new(gdi::GdiCapturer::new(display)?))
}
#[cfg(not(windows))]
pub fn create_capturer(_display: Display, _use_dxgi: bool, _gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
anyhow::bail!("Screen capture only supported on Windows")
}
/// Get all available displays
pub fn enumerate_displays() -> Result<Vec<Display>> {
display::enumerate_displays()
}
/// Get the primary display
pub fn primary_display() -> Result<Display> {
let displays = enumerate_displays()?;
displays
.into_iter()
.find(|d| d.is_primary)
.ok_or_else(|| anyhow::anyhow!("No primary display found"))
}