1 Plugin Development
nsportsman edited this page 2026-01-29 20:12:34 -06:00

Plugin Development

Guide to creating new protocol detection plugins for Nerva.

Plugin Architecture

Nerva uses a plugin architecture where each protocol has its own detection module. Plugins are registered at startup and executed against targets.

Plugin Interface

type Plugin interface {
    // Run performs service detection on an open connection
    Run(conn net.Conn, timeout time.Duration, target Target) (*Service, error)
    
    // PortPriority returns true if this is the default port for this protocol
    PortPriority(port uint16) bool
    
    // Name returns the protocol identifier (e.g., "ssh", "http")
    Name() string
    
    // Type returns the transport protocol
    Type() Protocol  // TCP, UDP, or SCTP
    
    // Priority returns execution priority (lower = run first)
    Priority() int
}

Creating a New Plugin

1. Create Plugin Directory

pkg/plugins/services/myprotocol/
├── myprotocol.go
└── myprotocol_test.go

2. Implement the Plugin

package myprotocol

import (
    "net"
    "time"

    "github.com/praetorian-inc/nerva/pkg/plugins"
)

type Plugin struct{}

func init() {
    plugins.RegisterPlugin(&Plugin{})
}

func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) {
    // Set read deadline
    conn.SetReadDeadline(time.Now().Add(timeout))
    
    // Send probe (if needed)
    probe := []byte("PROBE\r\n")
    _, err := conn.Write(probe)
    if err != nil {
        return nil, err
    }
    
    // Read response
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        return nil, err
    }
    response := buf[:n]
    
    // Validate protocol signature
    if !isMyProtocol(response) {
        return nil, nil  // Not this protocol
    }
    
    // Extract metadata
    metadata := ServiceMyProtocol{
        Version: extractVersion(response),
        Banner:  string(response),
    }
    
    // Return service
    return plugins.CreateServiceFrom(target, metadata, false, "", plugins.TCP), nil
}

func (p *Plugin) PortPriority(port uint16) bool {
    return port == 12345  // Default port for this protocol
}

func (p *Plugin) Name() string {
    return "myprotocol"
}

func (p *Plugin) Type() plugins.Protocol {
    return plugins.TCP
}

func (p *Plugin) Priority() int {
    return 100  // Medium priority
}

// Metadata structure
type ServiceMyProtocol struct {
    Version string `json:"version"`
    Banner  string `json:"banner"`
}

func isMyProtocol(data []byte) bool {
    // Check for protocol signature
    return len(data) > 0 && data[0] == 'M'
}

func extractVersion(data []byte) string {
    // Parse version from response
    return "1.0"
}

3. Register the Plugin

Add import to pkg/scan/plugin_list.go:

import (
    _ "github.com/praetorian-inc/nerva/pkg/plugins/services/myprotocol"
)

4. Write Tests

package myprotocol

import (
    "testing"
    
    "github.com/praetorian-inc/nerva/pkg/test"
)

func TestMyProtocol(t *testing.T) {
    // Use dockertest for integration testing
    // See existing plugins for examples
}

Best Practices

  1. Validate early — Return nil quickly if response doesn't match protocol
  2. Set timeouts — Always set read deadlines on connections
  3. Extract metadata — Capture as much useful information as possible
  4. Handle errors — Return appropriate errors for debugging
  5. Test thoroughly — Use dockertest for integration tests

Existing Plugins as Reference

  • Simple: pkg/plugins/services/echo/
  • TCP with banner: pkg/plugins/services/ftp/
  • Binary protocol: pkg/plugins/services/redis/
  • TLS detection: pkg/plugins/services/http/
  • UDP: pkg/plugins/services/dns/

See Also