# Agent WebSocket Protocol This document describes the binary WebSocket protocol used for communication between remote SNMP polling agents and the Towerops server. ## Overview The agent uses a **single persistent WebSocket connection** to: - Receive SNMP query jobs from the server - Submit raw SNMP query results back to the server - Send heartbeat updates - Report errors All messages use **Protocol Buffers** (protobuf) binary encoding for efficiency. ## Connection ### WebSocket URL ``` ws://towerops.net/socket/agent?token= wss://towerops.net/socket/agent?token= # Production (TLS) ``` ### Authentication - Token is passed as query parameter during WebSocket handshake - Server validates token and establishes session - Invalid tokens result in connection rejection ### Connection Lifecycle ``` 1. Agent connects with token 2. Server authenticates and assigns to channel "agent:" 3. Server immediately sends "jobs" message with initial job list 4. Agent executes jobs and sends "result" messages 5. Server pushes new jobs as devices are added/changed 6. Agent sends periodic "heartbeat" messages 7. Connection remains open indefinitely ``` ## Message Types All messages are binary protobuf-encoded. The WebSocket frame format is: ``` { event: "jobs" | "result" | "heartbeat" | "error", payload: } ``` ### Server → Agent #### `jobs` - Job List Sent when agent first connects or when jobs change. ```protobuf message AgentJobList { repeated AgentJob jobs = 1; } message AgentJob { string job_id = 1; // e.g., "discover:abc123" or "poll:def456" JobType job_type = 2; // DISCOVER or POLL string equipment_id = 3; // Equipment UUID SnmpDevice device = 4; // SNMP connection details repeated SnmpQuery queries = 5; // What to query } message SnmpDevice { string ip = 1; // 10.250.1.26 string community = 2; // SNMP community string string version = 3; // "1", "2c", or "3" uint32 port = 4; // 161 } message SnmpQuery { QueryType query_type = 1; // GET or WALK repeated string oids = 2; // OIDs to query } enum JobType { DISCOVER = 0; // Full device discovery POLL = 1; // Poll known sensors/interfaces } enum QueryType { GET = 0; // SNMP GET operation WALK = 1; // SNMP WALK operation } ``` **Example DISCOVER job:** ``` job_id: "discover:123e4567-e89b-12d3-a456-426614174000" job_type: DISCOVER equipment_id: "123e4567-e89b-12d3-a456-426614174000" device: { ip: "10.250.1.26" community: "public" version: "1" port: 161 } queries: [ { query_type: GET, oids: ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.2.0", ...] }, { query_type: WALK, oids: ["1.3.6.1.2.1.2.2.1"] }, { query_type: WALK, oids: ["1.3.6.1.4.1.41112"] } ] ``` **Example POLL job:** ``` job_id: "poll:123e4567-e89b-12d3-a456-426614174000" job_type: POLL equipment_id: "123e4567-e89b-12d3-a456-426614174000" device: { ip: "10.250.1.26" community: "public" version: "1" port: 161 } queries: [ { query_type: GET, oids: [ "1.3.6.1.4.1.41112.1.3.2.1.3.1", // Sensor 1 "1.3.6.1.4.1.41112.1.3.2.1.4.1", // Sensor 2 "1.3.6.1.2.1.2.2.1.10.3", // Interface 3 stats ... ]} ] ``` ### Agent → Server #### `result` - SNMP Query Results Sent after executing SNMP queries for a job. ```protobuf message SnmpResult { string equipment_id = 1; JobType job_type = 2; map oid_values = 3; // OID → value mapping int64 timestamp = 4; // Unix timestamp in seconds } ``` **Example:** ``` equipment_id: "123e4567-e89b-12d3-a456-426614174000" job_type: DISCOVER oid_values: { "1.3.6.1.2.1.1.1.0": "Linux AF11 2.6.33 ...", "1.3.6.1.2.1.1.2.0": "1.3.6.1.4.1.41112", "1.3.6.1.2.1.1.5.0": "AF11-Tower1", "1.3.6.1.2.1.2.2.1.2.1": "lo", "1.3.6.1.2.1.2.2.1.2.3": "eth0", "1.3.6.1.4.1.41112.1.3.2.1.3.1": "5725", // TX frequency ... } timestamp: 1705363200 ``` #### `heartbeat` - Agent Status Sent periodically (e.g., every 60 seconds) to indicate agent is alive and report metadata. ```protobuf message AgentHeartbeat { string version = 1; // Agent version (e.g., "1.0.0") string hostname = 2; // Agent hostname uint64 uptime_seconds = 3; // Agent uptime string ip_address = 4; // Agent's IP address } ``` **Example:** ``` version: "1.0.0" hostname: "datacenter-agent-1" uptime_seconds: 86400 ip_address: "192.168.1.100" ``` #### `error` - Job Execution Error Sent when a job fails (e.g., SNMP timeout, device unreachable). ```protobuf message AgentError { string equipment_id = 1; string job_id = 2; string message = 3; int64 timestamp = 4; } ``` **Example:** ``` equipment_id: "123e4567-e89b-12d3-a456-426614174000" job_id: "poll:123e4567-e89b-12d3-a456-426614174000" message: "SNMP timeout after 5 seconds" timestamp: 1705363200 ``` ## Agent Implementation Guide ### Minimal Rust Agent Architecture ```rust use tokio::net::TcpStream; use tokio_tungstenite::{connect_async, WebSocketStream}; use futures::{SinkExt, StreamExt}; use prost::Message; // 1. Connect to WebSocket let url = format!("ws://towerops.net/socket/agent?token={}", token); let (ws_stream, _) = connect_async(url).await?; let (mut write, mut read) = ws_stream.split(); // 2. Receive jobs while let Some(msg) = read.next().await { let msg = msg?; if msg.is_binary() { let frame: PhoenixMessage = serde_json::from_slice(&msg.into_data())?; if frame.event == "jobs" { let job_list = AgentJobList::decode(frame.payload)?; for job in job_list.jobs { tokio::spawn(execute_job(job, write.clone())); } } } } // 3. Execute SNMP job async fn execute_job(job: AgentJob, mut sink: SplitSink) { let mut oid_values = HashMap::new(); for query in job.queries { match query.query_type { QueryType::Get => { for oid in query.oids { let value = snmp_get(&job.device, &oid).await?; oid_values.insert(oid, value); } } QueryType::Walk => { for base_oid in query.oids { let results = snmp_walk(&job.device, &base_oid).await?; oid_values.extend(results); } } } } // 4. Send results back let result = SnmpResult { equipment_id: job.equipment_id, job_type: job.job_type, oid_values, timestamp: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64, }; let payload = result.encode_to_vec(); let frame = PhoenixMessage { event: "result".to_string(), payload, }; sink.send(Message::Binary(serde_json::to_vec(&frame)?)).await?; } // 5. Raw UDP SNMP implementation async fn snmp_get(device: &SnmpDevice, oid: &str) -> Result { // Construct SNMP GET PDU (BER encoded) // Send via UDP to device.ip:device.port // Parse response and extract value // Return value as string } async fn snmp_walk(device: &SnmpDevice, base_oid: &str) -> Result> { // Construct SNMP GETNEXT PDUs (BER encoded) // Send via UDP, follow GETNEXT chain // Parse responses and extract OID/value pairs // Return map of OID → value } ``` ### Phoenix Channel Message Format Phoenix channels use JSON-wrapped binary for messages: ```json { "event": "jobs", "payload": , "ref": null, "topic": "agent:abc123" } ``` The agent library should handle this framing automatically, or you can use `phoenix_client_rs` crate. ## Benefits Over REST API | Feature | REST API | WebSocket Channel | |---------|----------|-------------------| | Connection | New TCP handshake per request | Persistent connection | | Overhead | HTTP headers (~500 bytes) | Phoenix frame (~50 bytes) | | Latency | Round-trip per poll | Server push (0 latency) | | Encoding | JSON (~3x larger) | Protobuf (binary) | | Heartbeat | Explicit POST requests | Implicit (connection alive) | | Complexity | Multiple endpoints | Single channel | **Size comparison for a typical job:** - REST JSON: ~2.5 KB - WebSocket Protobuf: ~800 bytes - **Savings: 68%** ## Security Notes - Use `wss://` (WebSocket over TLS) in production - Token is validated once at connection time - Invalid tokens reject connection immediately - No need for per-message authentication ## Error Handling ### Connection Errors - **Token invalid**: Connection rejected, status 403 - **Network error**: Reconnect with exponential backoff - **Server restart**: Reconnect, re-authenticate ### Job Errors - **SNMP timeout**: Send `error` message with details - **Invalid OID**: Skip OID, log warning - **Device unreachable**: Send `error` message ## Testing ### Manual Testing with websocat ```bash # Install websocat brew install websocat # Connect and listen websocat "ws://localhost:4000/socket/agent?token=YOUR_TOKEN" # Server will send jobs message immediately ``` ### Unit Testing Mock the WebSocket connection and verify: 1. Agent sends correct protobuf on "result" event 2. Agent parses "jobs" message correctly 3. SNMP queries execute with correct parameters ## Protocol Version Current version: **1.0** Future versions may add: - Job priority queuing - Partial result streaming - Compression (gzip protobuf) - Multi-tenant isolation