Version: 1.0.0-draft
Date: 2026-02-09
Status: Draft
SocketPipe is a protocol specification for transporting terminal I/O between web-based SSH clients and backend services over WebSocket connections. It defines a standardized binary framing format, handshake procedure, and message types that enable interoperability between any compliant client and server implementation.
- Standardize communication between web SSH UIs (e.g., xterm.js, ghostty-wasm) and backend proxies
- Support both raw TCP tunneling and terminated SSH/PTY modes
- Provide a minimal, efficient binary protocol suitable for real-time terminal interaction
- Enable secure, authenticated connections with clear security boundaries
- Client UI implementation
- Server-side SSH credential storage mechanisms
- Logging and audit format specifications
- WebRTC data channel transport (future extension)
- Implementations MUST support WebSocket as the primary transport
- Implementations MUST use binary WebSocket frames (not text)
- Implementations MUST use big-endian (network byte order) for all multi-byte fields
Implementations MUST expose two distinct WebSocket endpoints:
| Endpoint | Purpose | TLS Requirement |
|---|---|---|
/tunnel |
Raw TCP tunnel (pass-through) | MAY use ws:// or wss:// |
/pty |
Terminated SSH with PTY I/O | MUST use wss:// only |
Rationale: The /tunnel endpoint carries already-encrypted SSH protocol bytes, so TLS is optional. The /pty endpoint carries plaintext terminal data, so TLS is mandatory.
- Implementations MUST support token-based authentication
- Clients MUST present a token (JWT or opaque) in the handshake message
- Servers MUST validate the token before establishing the connection
- Token format and validation logic are implementation-defined
┌─────────────────┐ WebSocket ┌─────────────────┐ TCP/SSH ┌─────────────┐
│ Web SSH Client │◄──────────────────►│ SocketPipe │◄────────────────►│ Backend │
│ (xterm.js, │ /tunnel or /pty │ Proxy │ │ (SSHd, │
│ ghostty-wasm) │ │ │ │ any TCP) │
└─────────────────┘ └─────────────────┘ └─────────────┘
- Proxy acts as a transparent TCP pipe
- WebSocket payload contains raw bytes forwarded to/from the target host:port
- Client is responsible for SSH protocol handling (handshake, encryption, authentication)
- Supports any TCP service, not limited to SSH
- Proxy terminates the SSH connection to the backend
- WebSocket payload contains plaintext PTY I/O (terminal data)
- Proxy manages SSH session lifecycle with the backend SSHd
- Supports terminal resize, signals, and environment variables
All messages use an 8-byte header followed by an optional payload:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Flags | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (variable) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Field | Size | Description |
|---|---|---|
| Type | 1 byte | Message type identifier |
| Flags | 1 byte | Message-specific flags |
| Reserved | 2 bytes | Reserved for future use; MUST be 0x0000 |
| Payload Length | 4 bytes | Length of payload in bytes (big-endian) |
| Payload | variable | Message-specific data |
| Type | Name | Direction | Description |
|---|---|---|---|
| 0x01 | HANDSHAKE_REQUEST | C→S | Client initiates connection |
| 0x02 | HANDSHAKE_RESPONSE | S→C | Server accepts/rejects connection |
| 0x10 | DATA | Bidirectional | Terminal/tunnel data |
| 0x20 | RESIZE | C→S | Terminal resize event |
| 0x21 | SIGNAL | C→S | Send signal to PTY (optional) |
| 0x22 | ENV | C→S | Set environment variable (optional) |
| 0x23 | FLOW_CONTROL | Bidirectional | XON/XOFF flow control (optional) |
| 0x30 | PING | Bidirectional | Keepalive request |
| 0x31 | PONG | Bidirectional | Keepalive response |
| 0x40 | CLOSE | Bidirectional | Graceful close |
| 0xF0 | ERROR | S→C | Error notification |
The first message after WebSocket connection MUST be a HANDSHAKE_REQUEST from the client.
Payload Structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version Major | Version Minor | Target Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ping Interval (seconds) | Ping Timeout (seconds) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Max Message Size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Host Length | Target Host (variable) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token Length (2 bytes) | Token (variable) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Field | Size | Description |
|---|---|---|
| Version Major | 1 byte | Protocol major version (current: 1) |
| Version Minor | 1 byte | Protocol minor version (current: 0) |
| Target Port | 2 bytes | Backend port number (big-endian) |
| Ping Interval | 2 bytes | Requested ping interval in seconds (0 = use default) |
| Ping Timeout | 2 bytes | Requested ping timeout in seconds (0 = use default) |
| Max Message Size | 4 bytes | Requested max message size in bytes (0 = use default) |
| Host Length | 1 byte | Length of Target Host string |
| Target Host | variable | Target hostname or IP (UTF-8) |
| Token Length | 2 bytes | Length of authentication token |
| Token | variable | Authentication token (opaque bytes) |
Defaults:
- Ping Interval: 30 seconds
- Ping Timeout: 10 seconds
- Max Message Size: 65536 bytes (64KB)
Server MUST respond with HANDSHAKE_RESPONSE.
Flags:
- Bit 0: Success (1) / Failure (0)
Payload Structure (Success):
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version Major | Version Minor | Ping Interval (seconds) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ping Timeout (seconds) | Max Message Size ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ... Max Message Size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Payload Structure (Failure):
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Error Code | Message Length | Message ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Carries terminal I/O or tunnel data.
Payload: Raw bytes to be forwarded.
Implementations MUST NOT send DATA messages larger than the negotiated Max Message Size.
Notifies server of terminal dimension change. Applicable to /pty endpoint only.
Payload:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Columns | Rows |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Pixel Width | Pixel Height |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
All fields are 2 bytes, big-endian.
Sends a signal to the PTY process. Applicable to /pty endpoint only.
Payload:
+-+-+-+-+-+-+-+-+
| Signal |
+-+-+-+-+-+-+-+-+
| Value | Signal |
|---|---|
| 0x01 | SIGINT |
| 0x02 | SIGTERM |
| 0x03 | SIGHUP |
| 0x04 | SIGKILL |
Sets an environment variable before shell execution. Applicable to /pty endpoint only; MUST be sent before first DATA message.
Payload:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Name Length | Name (variable) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Value Length (2 bytes) | Value (variable) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
XON/XOFF flow control signaling.
Flags:
- Bit 0: XON (1) / XOFF (0)
Payload: Empty.
Either side MAY send PING to verify connection liveness.
Payload: Optional opaque data (MUST be echoed in PONG).
Response to PING.
Payload: Echo of PING payload.
Requirements:
- Implementations MUST respond to PING with PONG within the negotiated timeout
- If no PONG is received within the timeout, the connection SHOULD be closed
- Implementations SHOULD send PING at the negotiated interval when no other traffic
Initiates graceful connection close.
Flags:
- Bit 0: Initiated by client (1) / server (0)
Payload:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reason Code | Message Length | Message ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Server reports an error condition.
Payload:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Error Code | Message Length | Message ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Range | Category |
|---|---|
| 1000-1999 | Authentication errors |
| 2000-2999 | Connection errors |
| 3000-3999 | Protocol errors |
| Code | Name | Description |
|---|---|---|
| 1000 | AUTH_FAILED | Token validation failed |
| 1001 | AUTH_EXPIRED | Token has expired |
| 1002 | AUTH_INSUFFICIENT | Token lacks required permissions |
| 2000 | CONNECT_FAILED | Failed to connect to backend |
| 2001 | CONNECT_TIMEOUT | Backend connection timed out |
| 2002 | CONNECT_REFUSED | Backend refused connection |
| 2003 | BACKEND_CLOSED | Backend closed connection |
| 3000 | PROTOCOL_ERROR | Generic protocol violation |
| 3001 | INVALID_MESSAGE | Malformed message received |
| 3002 | INVALID_STATE | Message not valid in current state |
| 3003 | MESSAGE_TOO_LARGE | Message exceeds max size |
| 3004 | UNSUPPORTED_VERSION | Protocol version not supported |
- Task 1.1.1: Define frame header structure and constants
- Dependencies: None
- Acceptance: Header struct with type, flags, reserved, length fields
- Task 1.1.2: Implement frame serialization (write)
- Dependencies: 1.1.1
- Acceptance: Can serialize any message type to bytes
- Task 1.1.3: Implement frame deserialization (read)
- Dependencies: 1.1.1
- Acceptance: Can deserialize bytes to message structs
- Task 1.1.4: Add validation (reserved=0, length bounds)
- Dependencies: 1.1.2, 1.1.3
- Acceptance: Invalid frames rejected with appropriate errors
- Task 1.1.5: Unit tests for frame parser
- Dependencies: 1.1.4
- Acceptance: 100% coverage of serialization/deserialization, edge cases
- Task 1.2.1: Define all message type structs
- Dependencies: 1.1.5
- Acceptance: Structs for all 11 message types
- Task 1.2.2: Implement HANDSHAKE_REQUEST/RESPONSE
- Dependencies: 1.2.1
- Acceptance: Can encode/decode handshake messages
- Task 1.2.3: Implement DATA message
- Dependencies: 1.2.1
- Acceptance: Can encode/decode data messages
- Task 1.2.4: Implement control messages (RESIZE, SIGNAL, ENV, FLOW_CONTROL)
- Dependencies: 1.2.1
- Acceptance: Can encode/decode all control messages
- Task 1.2.5: Implement PING/PONG messages
- Dependencies: 1.2.1
- Acceptance: Can encode/decode keepalive messages
- Task 1.2.6: Implement CLOSE/ERROR messages
- Dependencies: 1.2.1
- Acceptance: Can encode/decode session messages
- Task 1.2.7: Unit tests for all message types
- Dependencies: 1.2.2, 1.2.3, 1.2.4, 1.2.5, 1.2.6
- Acceptance: Full coverage of all message encode/decode paths
- Task 2.1.1: Define connection state enum
- Dependencies: Phase 1
- Acceptance: States: CONNECTING, HANDSHAKING, ESTABLISHED, CLOSING, CLOSED
- Task 2.1.2: Implement state transition logic
- Dependencies: 2.1.1
- Acceptance: Valid transitions enforced, invalid transitions rejected
- Task 2.1.3: State machine unit tests
- Dependencies: 2.1.2
- Acceptance: All valid/invalid transitions tested
- Task 2.2.1: Implement client handshake initiator
- Dependencies: 2.1.3
- Acceptance: Client sends HANDSHAKE_REQUEST on connect
- Task 2.2.2: Implement server handshake responder
- Dependencies: 2.1.3
- Acceptance: Server validates and responds with HANDSHAKE_RESPONSE
- Task 2.2.3: Implement parameter negotiation
- Dependencies: 2.2.1, 2.2.2
- Acceptance: Ping interval, timeout, max size negotiated correctly
- Task 2.2.4: Integration tests for handshake
- Dependencies: 2.2.3
- Acceptance: Client-server handshake succeeds with various parameters
- Task 3.1.1: Implement ping timer
- Dependencies: Phase 2
- Acceptance: PING sent at negotiated interval
- Task 3.1.2: Implement pong handler
- Dependencies: 3.1.1
- Acceptance: PONG received resets timeout
- Task 3.1.3: Implement timeout detection
- Dependencies: 3.1.2
- Acceptance: Connection closed if PONG not received within timeout
- Task 3.1.4: Keepalive integration tests
- Dependencies: 3.1.3
- Acceptance: Connections stay alive with traffic, timeout without
- Task 3.2.1: Define error code constants
- Dependencies: 3.1.4
- Acceptance: All error codes from spec defined
- Task 3.2.2: Implement error message creation
- Dependencies: 3.2.1
- Acceptance: Can create ERROR messages with code and text
- Task 3.2.3: Implement graceful close flow
- Dependencies: 3.2.2
- Acceptance: CLOSE message sent, connection terminated cleanly
- Task 3.2.4: Error handling integration tests
- Dependencies: 3.2.3
- Acceptance: Errors reported correctly, connections closed gracefully
- Task 4.1.1: Implement TCP connection to backend
- Dependencies: Phase 3
- Acceptance: Can connect to arbitrary host:port
- Task 4.1.2: Implement bidirectional data forwarding
- Dependencies: 4.1.1
- Acceptance: Data flows both directions without modification
- Task 4.1.3: Implement connection cleanup
- Dependencies: 4.1.2
- Acceptance: Both sides closed when either disconnects
- Task 4.1.4: Tunnel endpoint integration tests
- Dependencies: 4.1.3
- Acceptance: Can tunnel SSH, HTTP, other TCP protocols
- Task 4.2.1: Implement SSH client connection
- Dependencies: 4.1.4
- Acceptance: Can connect to SSHd with credentials from token
- Task 4.2.2: Implement PTY allocation
- Dependencies: 4.2.1
- Acceptance: Shell session started with PTY
- Task 4.2.3: Implement terminal resize handling
- Dependencies: 4.2.2
- Acceptance: RESIZE messages update PTY dimensions
- Task 4.2.4: Implement signal forwarding (optional)
- Dependencies: 4.2.3
- Acceptance: SIGNAL messages sent to PTY process
- Task 4.2.5: Implement environment variable setting (optional)
- Dependencies: 4.2.3
- Acceptance: ENV messages set variables before shell start
- Task 4.2.6: PTY endpoint integration tests
- Dependencies: 4.2.5
- Acceptance: Full terminal session works with resize and signals
- Task 5.1.1: Implement message size limits
- Dependencies: Phase 4
- Acceptance: Messages exceeding max size rejected
- Task 5.1.2: Implement host/port validation
- Dependencies: 5.1.1
- Acceptance: Invalid targets rejected, allowlists supported
- Task 5.1.3: Implement token validation interface
- Dependencies: 5.1.2
- Acceptance: Pluggable token validator with JWT example
- Task 5.1.4: Security integration tests
- Dependencies: 5.1.3
- Acceptance: Invalid inputs rejected, valid inputs accepted
- Task 5.2.1: Implement TLS requirement for
/pty- Dependencies: 5.1.4
- Acceptance: Non-TLS connections to
/ptyrejected
- Task 5.2.2: Document TLS configuration
- Dependencies: 5.2.1
- Acceptance: Clear docs for certificate setup
- Task 5.2.3: TLS integration tests
- Dependencies: 5.2.2
- Acceptance: TLS enforcement verified
- Frame parser: serialization, deserialization, validation
- Message types: all encode/decode paths
- State machine: all transitions
- Error handling: all error codes
- Handshake: success, failure, version mismatch
- Keepalive: alive, timeout, recovery
- Tunnel: connect, forward, disconnect
- PTY: session, resize, signals, env
- Reference test vectors for all message types
- Interoperability tests between implementations
- WebSocket server capable of binary frames
- TCP connectivity to backend services
- TLS termination (required for
/pty, recommended for/tunnel)
- WebSocket client with binary frame support
- For
/tunnel: SSH client library (e.g., libssh, ssh2) - For
/pty: Terminal emulator (e.g., xterm.js, ghostty-wasm)
The following features are candidates for future specification versions:
- Session Resume: Reconnection with session ID for network disruption recovery
- Session Persistence: Long-lived sessions surviving indefinite disconnection
- WebRTC Data Channel: Alternative transport for lower latency
- Multi-session Multiplexing: Multiple logical sessions over single connection
- File Transfer: SCP/SFTP support
- Port Forwarding: SSH port forwarding tunnels
To start implementation, type: "implement SPECIFICATION.md"