Implement GuruRMM Phase 1: Real-time tunnel infrastructure
Complete bidirectional tunnel communication between server and agents, enabling persistent secure channels for future command execution and file operations. Agents transition from heartbeat mode to tunnel mode on-demand while maintaining WebSocket connection. Server Implementation: - Database layer (db/tunnel.rs): Session CRUD, ownership validation, cleanup on disconnect (prevents orphaned sessions) - API endpoints (api/tunnel.rs): POST /open, POST /close, GET /status with JWT auth, UUID validation, proper HTTP status codes - Protocol extension (ws/mod.rs): TunnelOpen/Close/Data messages, agent response handlers (TunnelReady/Data/Error) - Migration (006_tunnel_sessions.sql): tech_sessions table with partial unique constraint, foreign keys with CASCADE, audit table Agent Implementation: - State machine (tunnel/mod.rs): AgentMode (Heartbeat ↔ Tunnel), channel multiplexing, concurrent session prevention - WebSocket handlers (transport/websocket.rs): Open/close tunnel, mode switching without dropping connection, cleanup on disconnect - Protocol extension (transport/mod.rs): TunnelReady/Data/Error messages matching server definitions - Unit tests: Lifecycle and channel management coverage Key Features: - Security: JWT auth, session ownership verification, SQL injection prevention, constraint-based duplicate session blocking - Cleanup: Automatic session closure on agent disconnect (both sides), channel cleanup, graceful state transitions - Error handling: Proper HTTP status codes (400/403/404/409/500), comprehensive Result types, detailed logging - Extensibility: Channel types ready (Terminal/File/Registry/Service), TunnelDataPayload enum for Phase 2+ expansion Phase 1 Scope (Implemented): - Tunnel session lifecycle management - Mode switching (heartbeat ↔ tunnel) - Protocol message routing - Database session tracking Phase 2 Next Steps: - Terminal command execution (tokio::process::Command) - Client WebSocket connections for output streaming - Command audit logging - File transfer operations Verification: - Server compiles successfully (0 errors) - Agent unit tests pass (tunnel lifecycle, channel management) - Code review approved (protocol alignment verified) - Database constraints enforce referential integrity - Cleanup tested (session closure on disconnect) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,9 @@ pub enum AgentMessage {
|
||||
WatchdogEvent(WatchdogEventPayload),
|
||||
UpdateResult(UpdateResultPayload),
|
||||
Heartbeat,
|
||||
TunnelReady { session_id: String },
|
||||
TunnelData { channel_id: String, data: TunnelDataPayload },
|
||||
TunnelError { channel_id: String, error: String },
|
||||
}
|
||||
|
||||
/// Messages from server to agent
|
||||
@@ -98,6 +101,9 @@ pub enum ServerMessage {
|
||||
Update(UpdatePayload),
|
||||
Ack { message_id: Option<String> },
|
||||
Error { code: String, message: String },
|
||||
TunnelOpen { session_id: String, tech_id: Uuid },
|
||||
TunnelClose { session_id: String },
|
||||
TunnelData { channel_id: String, data: TunnelDataPayload },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -297,6 +303,21 @@ pub struct UpdateResultPayload {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Tunnel data payload types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TunnelDataPayload {
|
||||
/// Terminal command execution (Phase 1)
|
||||
Terminal { command: String },
|
||||
/// Terminal output response
|
||||
TerminalOutput {
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
exit_code: Option<i32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Result of successful agent authentication
|
||||
struct AuthResult {
|
||||
agent_id: Uuid,
|
||||
@@ -479,7 +500,25 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
|
||||
|
||||
// Cleanup
|
||||
state.agents.write().await.remove(&agent_id);
|
||||
let _ = db::update_agent_status(&state.db, agent_id, "offline").await;
|
||||
|
||||
// Update agent status
|
||||
if let Err(e) = db::update_agent_status(&state.db, agent_id, "offline").await {
|
||||
error!("Failed to update agent status for {}: {}", agent_id, e);
|
||||
}
|
||||
|
||||
// Close all active tunnel sessions for this agent
|
||||
match db::close_agent_tunnel_sessions(&state.db, agent_id).await {
|
||||
Ok(count) if count > 0 => {
|
||||
info!("Closed {} active tunnel session(s) for agent {}", count, agent_id);
|
||||
}
|
||||
Ok(_) => {
|
||||
debug!("No active tunnel sessions to close for agent {}", agent_id);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to close tunnel sessions for agent {}: {}", agent_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
send_task.abort();
|
||||
|
||||
info!("Agent {} connection closed", agent_id);
|
||||
@@ -745,6 +784,57 @@ async fn handle_agent_message(
|
||||
}
|
||||
}
|
||||
|
||||
AgentMessage::TunnelReady { session_id } => {
|
||||
info!(
|
||||
"Agent {} tunnel ready: session_id={}",
|
||||
agent_id, session_id
|
||||
);
|
||||
|
||||
// Update session activity timestamp
|
||||
if let Err(e) = db::update_session_activity(&state.db, &session_id).await {
|
||||
error!(
|
||||
"Failed to update session activity for {}: {}",
|
||||
session_id, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AgentMessage::TunnelData { channel_id, data } => {
|
||||
debug!(
|
||||
"Received tunnel data from agent {}: channel_id={}, type={:?}",
|
||||
agent_id, channel_id, data
|
||||
);
|
||||
|
||||
// Phase 2: Forward data to connected clients via WebSocket or REST API
|
||||
// For now, just log the data
|
||||
match data {
|
||||
TunnelDataPayload::TerminalOutput { stdout, stderr, exit_code } => {
|
||||
if !stdout.is_empty() {
|
||||
debug!("Terminal stdout: {}", stdout.trim());
|
||||
}
|
||||
if !stderr.is_empty() {
|
||||
debug!("Terminal stderr: {}", stderr.trim());
|
||||
}
|
||||
if let Some(code) = exit_code {
|
||||
debug!("Terminal exit code: {}", code);
|
||||
}
|
||||
}
|
||||
TunnelDataPayload::Terminal { command } => {
|
||||
debug!("Terminal command echo: {}", command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AgentMessage::TunnelError { channel_id, error } => {
|
||||
error!(
|
||||
"Tunnel error from agent {}: channel_id={}, error={}",
|
||||
agent_id, channel_id, error
|
||||
);
|
||||
|
||||
// Phase 2: Forward error to connected clients
|
||||
// For now, just log the error
|
||||
}
|
||||
|
||||
AgentMessage::Auth(_) => {
|
||||
warn!("Received unexpected auth message from already authenticated agent");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user