Skip to content

Adding a New Protocol

lex edited this page May 28, 2026 · 1 revision

Adding a New Protocol

This guide walks you through adding support for a new wire protocol to Ocular.


Overview

Adding a protocol requires changes in 4 files (all in crates/ocular-protocol/) plus tests. No changes needed in the proxy, capture, or TUI crates — the ProtocolHandler trait abstracts everything.


Step 1: Create the Parser Module

Create crates/ocular-protocol/src/yourprotocol.rs:

/// Parse a request buffer into a human-readable command string.
/// Return None if the buffer doesn't contain a complete parseable request.
pub fn parse_request(buf: &[u8]) -> Option<String> {
    // Example: parse a text-based protocol
    let text = std::str::from_utf8(buf).ok()?;
    let line = text.lines().next()?;
    Some(line.to_string())
}

/// Parse a response buffer into a human-readable summary.
/// Return None if unparseable.
pub fn parse_response(buf: &[u8]) -> Option<String> {
    let text = std::str::from_utf8(buf).ok()?;
    Some(text.lines().next()?.to_string())
}

/// Format the full response for the detail pane.
/// Return None to use the default (raw bytes as UTF-8).
pub fn format_response_detail(buf: &[u8]) -> Option<String> {
    Some(String::from_utf8_lossy(buf).to_string())
}

/// Extract the full command string for display and filtering.
/// For SQL-like protocols, this is the full query text.
pub fn extract_full_command(buf: &[u8]) -> Option<String> {
    parse_request(buf)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_request() {
        let buf = b"GET key1\r\n";
        assert_eq!(parse_request(buf), Some("GET key1".to_string()));
    }

    #[test]
    fn test_parse_incomplete_request() {
        let buf = b"GET ke";
        assert_eq!(parse_request(buf), None);
    }
}

Step 2: Implement the ProtocolHandler Trait

In crates/ocular-protocol/src/handlers.rs, add:

use crate::yourprotocol;

pub struct YourProtocolHandler;

impl ProtocolHandler for YourProtocolHandler {
    fn parse_request(&self, buf: &[u8]) -> Option<String> {
        yourprotocol::parse_request(buf)
    }

    fn parse_response(&self, buf: &[u8]) -> Option<String> {
        yourprotocol::parse_response(buf)
    }

    fn format_response_detail(&self, buf: &[u8]) -> Option<String> {
        yourprotocol::format_response_detail(buf)
    }

    fn extract_full_command(&self, buf: &[u8]) -> Option<String> {
        yourprotocol::extract_full_command(buf)
    }

    fn default_port(&self) -> u16 {
        12345 // The well-known port for your protocol
    }

    // If your protocol needs multi-packet buffering:
    // fn needs_request_buffering(&self) -> bool { true }
    // fn needs_response_buffering(&self) -> bool { true }
    // fn request_complete(&self, buf: &[u8]) -> bool { /* check delimiter */ }
    // fn response_complete(&self, buf: &[u8]) -> bool { /* check length header */ }

    // If your protocol has a handshake (like MySQL auth):
    // fn capture_handshake(&self, buf: &[u8]) -> Option<HandshakeAction> {
    //     Some(HandshakeAction::Done)
    // }
}

Step 3: Register the Protocol

In crates/ocular-protocol/src/lib.rs:

3a. Add the module

pub mod yourprotocol;

3b. Add enum variant

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol {
    Redis,
    Mysql,
    Postgres,
    Amqp,
    Mongodb,
    Memcached,
    Kafka,
    Http,
    YourProtocol,  // ← new
}

3c. Add name mapping in from_str

impl Protocol {
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "redis" | "resp" => Some(Self::Redis),
            "mysql" => Some(Self::Mysql),
            // ...
            "yourprotocol" | "yp" => Some(Self::YourProtocol),  // ← new (add alias)
            _ => None,
        }
    }
}

3d. Add handler in get_handler

pub fn get_handler(protocol: Protocol) -> Box<dyn ProtocolHandler> {
    match protocol {
        Protocol::Redis => Box::new(handlers::RedisHandler),
        // ...
        Protocol::YourProtocol => Box::new(handlers::YourProtocolHandler),  // ← new
    }
}

Step 4: Handle Buffering (If Needed)

Most text-based protocols (line-delimited) don't need special buffering. But if your protocol:

  • Uses length-prefixed messages (like Kafka, MySQL): implement message_length() so capture mode knows where one message ends and the next begins
  • Has multi-packet responses (large result sets): implement needs_response_buffering() = true and response_complete()
  • Has a handshake phase (like MySQL auth): implement capture_handshake() to tell capture mode when to start parsing data

Example for a length-prefixed protocol:

fn message_length(&self, buf: &[u8]) -> Option<usize> {
    if buf.len() < 4 { return None; }
    let len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
    Some(4 + len) // header + body
}

Step 5: Test

cargo test -p ocular-protocol

Integration test with a real service

# Start the service
docker run -d --rm -p 12345:12345 yourservice/yourimage

# Test with Ocular
ocular proxy yourprotocol 127.0.0.1:12345

Checklist

  • Parser module created with parse_request, parse_response, format_response_detail, extract_full_command
  • ProtocolHandler trait implemented
  • Protocol registered in lib.rs (module, enum, from_str, get_handler)
  • Unit tests for parser edge cases (empty buffer, incomplete message, malformed data)
  • default_port() returns the well-known port
  • Buffering methods implemented if needed
  • cargo test passes
  • Manual test with real service via ocular proxy

Clone this wiki locally