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>
122 lines
4.2 KiB
Rust
122 lines
4.2 KiB
Rust
//! Viewer module - Native remote desktop viewer with full keyboard capture
|
|
//!
|
|
//! This module provides the viewer functionality for connecting to remote
|
|
//! GuruConnect sessions with low-level keyboard hooks for Win key capture.
|
|
|
|
mod input;
|
|
mod render;
|
|
mod transport;
|
|
|
|
use crate::proto;
|
|
use anyhow::Result;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{mpsc, Mutex};
|
|
use tracing::{info, error, warn};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ViewerEvent {
|
|
Connected,
|
|
Disconnected(String),
|
|
Frame(render::FrameData),
|
|
CursorPosition(i32, i32, bool),
|
|
CursorShape(proto::CursorShape),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum InputEvent {
|
|
Mouse(proto::MouseEvent),
|
|
Key(proto::KeyEvent),
|
|
SpecialKey(proto::SpecialKeyEvent),
|
|
}
|
|
|
|
/// Run the viewer to connect to a remote session
|
|
pub async fn run(server_url: &str, session_id: &str, api_key: &str) -> Result<()> {
|
|
info!("GuruConnect Viewer starting");
|
|
info!("Server: {}", server_url);
|
|
info!("Session: {}", session_id);
|
|
|
|
// Create channels for communication between components
|
|
let (viewer_tx, viewer_rx) = mpsc::channel::<ViewerEvent>(100);
|
|
let (input_tx, input_rx) = mpsc::channel::<InputEvent>(100);
|
|
|
|
// Connect to server
|
|
let ws_url = format!("{}?session_id={}", server_url, session_id);
|
|
info!("Connecting to {}", ws_url);
|
|
|
|
let (ws_sender, mut ws_receiver) = transport::connect(&ws_url, api_key).await?;
|
|
let ws_sender = Arc::new(Mutex::new(ws_sender));
|
|
|
|
info!("Connected to server");
|
|
let _ = viewer_tx.send(ViewerEvent::Connected).await;
|
|
|
|
// Clone sender for input forwarding
|
|
let ws_sender_input = ws_sender.clone();
|
|
|
|
// Spawn task to forward input events to server
|
|
let mut input_rx = input_rx;
|
|
let input_task = tokio::spawn(async move {
|
|
while let Some(event) = input_rx.recv().await {
|
|
let msg = match event {
|
|
InputEvent::Mouse(m) => proto::Message {
|
|
payload: Some(proto::message::Payload::MouseEvent(m)),
|
|
},
|
|
InputEvent::Key(k) => proto::Message {
|
|
payload: Some(proto::message::Payload::KeyEvent(k)),
|
|
},
|
|
InputEvent::SpecialKey(s) => proto::Message {
|
|
payload: Some(proto::message::Payload::SpecialKey(s)),
|
|
},
|
|
};
|
|
|
|
if let Err(e) = transport::send_message(&ws_sender_input, &msg).await {
|
|
error!("Failed to send input: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Spawn task to receive messages from server
|
|
let viewer_tx_recv = viewer_tx.clone();
|
|
let receive_task = tokio::spawn(async move {
|
|
while let Some(msg) = ws_receiver.recv().await {
|
|
match msg.payload {
|
|
Some(proto::message::Payload::VideoFrame(frame)) => {
|
|
if let Some(proto::video_frame::Encoding::Raw(raw)) = frame.encoding {
|
|
let frame_data = render::FrameData {
|
|
width: raw.width as u32,
|
|
height: raw.height as u32,
|
|
data: raw.data,
|
|
compressed: raw.compressed,
|
|
is_keyframe: raw.is_keyframe,
|
|
};
|
|
let _ = viewer_tx_recv.send(ViewerEvent::Frame(frame_data)).await;
|
|
}
|
|
}
|
|
Some(proto::message::Payload::CursorPosition(pos)) => {
|
|
let _ = viewer_tx_recv.send(ViewerEvent::CursorPosition(
|
|
pos.x, pos.y, pos.visible
|
|
)).await;
|
|
}
|
|
Some(proto::message::Payload::CursorShape(shape)) => {
|
|
let _ = viewer_tx_recv.send(ViewerEvent::CursorShape(shape)).await;
|
|
}
|
|
Some(proto::message::Payload::Disconnect(d)) => {
|
|
warn!("Server disconnected: {}", d.reason);
|
|
let _ = viewer_tx_recv.send(ViewerEvent::Disconnected(d.reason)).await;
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Run the window (this blocks until window closes)
|
|
render::run_window(viewer_rx, input_tx).await?;
|
|
|
|
// Cleanup
|
|
input_task.abort();
|
|
receive_task.abort();
|
|
|
|
Ok(())
|
|
}
|