gRPC Plugin Architecture
Relevant source files
The following files were used as context for generating this wiki page:
The gRPC Plugin Architecture in sshpiper provides a decoupled and extensible mechanism for managing SSH session routing and authentication. In this architecture, sshpiperd acts as a gRPC client that communicates with one or more plugin servers. These plugins are responsible for determining how a downstream connection (the client) should be authenticated and subsequently routed to an upstream connection (the destination server).
The system supports two primary deployment models for plugins: local child processes that communicate via stdin/stdout wrapped in ioconn, and remote gRPC services connected over a network. This flexibility allows developers to implement complex routing logic, multi-factor authentication (MFA), and dynamic upstream mapping using any language that supports gRPC.
Sources: CONTRIBUTING.md:46-52, README.md:164-170, cmd/sshpiperd/internal/plugin/grpc.go:425-455
Core Components and Protocol
The architecture is defined by a Protocol Buffers schema that outlines the SshPiperPlugin service. This service facilitates various callbacks during the lifecycle of an SSH connection, ranging from initial connection notice to final pipe establishment or error reporting.
Communication Flow
The following sequence diagram illustrates the typical interaction between sshpiperd and a plugin during a password authentication attempt.
The interaction starts with metadata exchange and proceeds through a series of specific RPC calls depending on the authentication method chosen by the client.
Sources: libplugin/plugin.proto:52-66, cmd/sshpiperd/internal/plugin/grpc.go:66-100
Key Data Structures
The primary data structure for tracking session context is ConnMeta, which is passed to almost every gRPC call to ensure the plugin can maintain state for a specific connection.
| Field | Type | Description |
|---|---|---|
user_name | string | The username provided by the downstream client. |
from_addr | string | The source IP address and port of the client. |
uniq_id | string | A unique UUID generated for the session. |
metadata | map<string, string> | A key-value store for passing custom session state. |
Sources: libplugin/plugin.proto:7-12, cmd/sshpiperd/internal/plugin/grpc.go:138-148
Plugin Lifecycle and Callbacks
sshpiperd queries a plugin for its supported capabilities using ListCallbacks. Based on the response, it installs specific handlers into its internal PiperConfig.
Authentication Callbacks
Plugins can implement several authentication methods:
- NoneAuth: Handles users attempting to connect without credentials.
- PasswordAuth: Receives the plaintext password for validation or forwarding.
- PublicKeyAuth: Receives the client's public key blob.
- KeyboardInteractiveAuth: A bidirectional stream allowing the plugin to send prompts (like MFA questions) and receive answers.
Sources: libplugin/plugin.proto:89-130, cmd/sshpiperd/internal/plugin/grpc.go:60-120
Connection Management Callbacks
Beyond authentication, plugins receive notices about the state of the session:
- PipeStartNotice: Called when the connection to the upstream is successfully established.
- PipeErrorNotice: Called when an established session encounters a network error.
- PipeCreateErrorNotice: Called if
sshpiperdfails to initiate a connection to the designated upstream.
Sources: libplugin/plugin.proto:170-192, cmd/sshpiperd/internal/plugin/grpc.go:275-298
Plugin Chaining
sshpiperd supports chaining multiple plugins together. In this configuration, each plugin in the chain can perform its own checks or challenges before passing the connection to the next.
The ChainPlugins structure manages this by iterating through a slice of GrpcPluginConfig objects. A plugin can signal to move to the next step by returning an Upstream object containing a NextPluginAuth message.
Sources: cmd/sshpiperd/internal/plugin/chain.go:15-32, libplugin/plugin.proto:44-46
Upstream Configuration
When a plugin approves a connection, it returns an Upstream message. This message defines how sshpiperd should connect to the final destination.
| Field | Description |
|---|---|
uri | The network address (e.g., tcp://10.0.0.1:22). |
user_name | The username to use for the upstream connection. |
ignore_host_key | If true, sshpiperd will skip upstream host key verification. |
auth | A oneof field containing none, password, private_key, or remote_signer. |
Sources: libplugin/plugin.proto:14-34, cmd/sshpiperd/internal/plugin/grpc.go:210-250
Remote Signer Auth
A specialized auth type UpstreamRemoteSignerAuth allows sshpiperd to perform authentication using a key held by the plugin or an external signer, without the private key ever leaving the plugin's environment. sshpiperd acts as a proxy for the signing request via the grpcsigner client.
Sources: cmd/sshpiperd/internal/plugin/grpc.go:265-272, libplugin/plugin.proto:40-42
Logging and Debugging
The architecture includes a built-in logging mechanism where plugins can stream their internal logs back to sshpiperd via the Logs RPC. This allows sshpiperd to aggregate logs from all active plugins into its primary output stream (typically stdout or stderr).
func (g *GrpcPlugin) RecvLogs(writer io.Writer) error {
stream, err := g.client.Logs(context.Background(), &libplugin.StartLogRequest{
UniqId: uid.String(),
Level: log.GetLevel().String(),
Tty: checkIfTerminal(log.StandardLogger().Out),
})
// ... loop and Fprintln to writer
}
Sources: cmd/sshpiperd/internal/plugin/grpc.go:300-325, libplugin/plugin.proto:68-76
The gRPC Plugin Architecture ensures that sshpiperd remains a thin, high-performance proxy while delegating complex, environment-specific business logic to modular plugins. By using standardized gRPC interfaces and a robust metadata sharing mechanism, it supports everything from simple static routing to complex multi-plugin security chains.