Documentation
¶
Overview ¶
Package sshx manages persistent SSH connections.
The core of the package is the Pool/Managed pair: one self-healing connection per host, reconnecting with jittered exponential backoff, capped dial concurrency across the pool, and non-blocking not-ready semantics so a dead host never stalls a caller polling many. Around that core sit the pieces a persistent connection needs: Dial with context cancellation, one-shot execution shaped like os/exec (Client.Output, Client.CombinedOutput), stream-based interactive sessions (Client.Shell), and fail-closed host-key verification (KnownHosts, TOFU, InsecureAcceptAny).
The package is headless by design: it never touches the process's terminal, environment, or standard streams. Interactive decisions — confirming an unknown host key, supplying a passphrase — are callbacks the consumer wires to a terminal, a GUI, or an automated policy. Private-key parsing, loading, and generation live in the keys subpackage.
Index ¶
- Constants
- Variables
- func InsecureAcceptAny() ssh.HostKeyCallback
- func IsAuthFailure(err error) bool
- func KnownHosts(path string) (ssh.HostKeyCallback, error)
- func TOFU(path string, confirm ConfirmHostFunc) (ssh.HostKeyCallback, error)
- type Client
- func (c *Client) Close() error
- func (c *Client) CombinedOutput(ctx context.Context, cmd string) (string, error)
- func (c *Client) Output(ctx context.Context, cmd string) (Result, error)
- func (c *Client) Ping(ctx context.Context) error
- func (c *Client) Shell(ctx context.Context, cfg SessionConfig) (*Session, error)
- type Config
- type ConfirmHostFunc
- type DialError
- type HostInfo
- type HostKeyMismatchError
- type Managed
- type ManagedConfig
- type Pool
- type Result
- type Session
- type SessionConfig
- type State
- type TTYConfig
- type UnknownHostKeyError
Constants ¶
const ( // StageNetwork: the TCP connection could not be established. StageNetwork = "network" // StageHostKey: the transport came up but the host-key policy refused the // server's identity. The policy's error is retrievable with errors.As. StageHostKey = "hostkey" // StageHandshake: the SSH handshake failed after host-key verification // passed — authentication exhaustion, protocol failure, or peer close. StageHandshake = "handshake" )
Dial stages reported by DialError.Stage. The stage states with certainty where a dial died; it never guesses at causes inside a stage.
const ( DefaultDialTimeout = 10 * time.Second DefaultPingTimeout = 5 * time.Second DefaultTerm = "xterm-256color" DefaultCols = 80 DefaultRows = 24 DefaultMaxDials = 16 )
Defaults applied wherever the caller leaves a zero value: Dial and Ping when ctx carries no deadline, TTYConfig fields left empty, and NewPool given a non-positive cap.
Variables ¶
var ErrAuthRequired = errors.New("sshx: at least one auth method required")
ErrAuthRequired is returned by Dial when Config.Auth is empty.
var ErrClosed = errors.New("sshx: closed")
ErrClosed is returned for operations on a closed client, session, or pool.
var ErrHostKeyRequired = errors.New("sshx: host key policy required")
ErrHostKeyRequired is returned by Dial when Config.HostKey is nil. There is no insecure default; opting out takes an explicit InsecureAcceptAny.
var ErrNotReady = errors.New("sshx: connection not ready")
ErrNotReady is returned by Managed when no live connection exists right now (connecting, backing off, or reconnecting). It is returned immediately, never after blocking.
Functions ¶
func InsecureAcceptAny ¶
func InsecureAcceptAny() ssh.HostKeyCallback
InsecureAcceptAny returns a verifier that accepts every host key without checking anything. It exists for lab use; production traffic has no business anywhere near it.
func IsAuthFailure ¶ added in v0.1.1
IsAuthFailure reports whether err represents an SSH authentication rejection — key not accepted, wrong password, no method left to try. It is a heuristic, not a guarantee: x/crypto exposes no typed auth error, so this is the one place in the module that inspects error text, pinned by tests against real rejections so an upstream rewording breaks here, loudly, instead of silently in every consumer.
func KnownHosts ¶
func KnownHosts(path string) (ssh.HostKeyCallback, error)
KnownHosts returns a strict host-key verifier pinned to the OpenSSH-format file at path: unknown hosts fail with UnknownHostKeyError, changed keys with HostKeyMismatchError. The file is created empty (0600, directory 0700) if absent.
func TOFU ¶
func TOFU(path string, confirm ConfirmHostFunc) (ssh.HostKeyCallback, error)
TOFU returns a trust-on-first-use verifier over the file at path: known hosts are checked strictly, and an unknown host is handed to confirm, whose consent pins the key. A nil confirm degrades to strict. A changed key is never confirmable.
Concurrent first-contact dials through one TOFU value collapse to a single confirmation and a single pinned line. That guarantee is per returned callback: use one TOFU value per known_hosts file, not one per dial.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is one authenticated SSH connection. Command execution and sessions multiplex over it; only Dial pays the handshake.
func Dial ¶
Dial connects to addr (host:port) and authenticates. ctx bounds the whole dial — TCP connect and SSH handshake; without a deadline DefaultDialTimeout applies. Failures are reported as *DialError with the stage that died, and host-key policy errors are retrievable from it with errors.As.
func (*Client) Close ¶
Close terminates the connection and stops the keepalive goroutine. It is idempotent; every call returns the first close's error.
func (*Client) CombinedOutput ¶
CombinedOutput runs cmd like Output but folds stdout and stderr into one string, preserving their interleaving.
func (*Client) Output ¶
Output runs cmd in a fresh session over the shared transport and returns stdout and stderr separately. Like os/exec, output captured before a failure is still returned: a non-zero exit populates Result and returns the wrapped *ssh.ExitError. ctx cancellation closes the in-flight session (the remote command is abandoned, not killed) and returns ctx's error.
func (*Client) Ping ¶
Ping sends an OpenSSH keepalive request and reports whether the peer answered. It is deadline-guarded — DefaultPingTimeout unless ctx sets less — so a black-hole peer that never replies cannot wedge the caller (golang/go#21478).
type Config ¶
type Config struct {
User string
Auth []ssh.AuthMethod
HostKey ssh.HostKeyCallback
}
Config configures a Dial. HostKey and at least one Auth method are required; there are no insecure defaults.
type ConfirmHostFunc ¶
ConfirmHostFunc decides whether a previously unseen host is trusted. Returning true pins the key; false or an error refuses the connection.
type DialError ¶
type DialError struct {
Stage string // StageNetwork, StageHostKey, or StageHandshake
Addr string
Err error
}
DialError reports a failed Dial with the stage it died in.
type HostInfo ¶
type HostInfo struct {
Host string // address as dialed, host:port
Remote net.Addr
KeyType string // e.g. "ssh-ed25519"
Fingerprint string // SHA-256, OpenSSH format
Key ssh.PublicKey
}
HostInfo describes a host presenting a key, as handed to a ConfirmHostFunc.
type HostKeyMismatchError ¶
HostKeyMismatchError reports a host whose presented key differs from the pinned one — a possible man-in-the-middle. It is never confirmable.
func (*HostKeyMismatchError) Error ¶
func (e *HostKeyMismatchError) Error() string
Error implements error.
type Managed ¶
type Managed struct {
// contains filtered or unexported fields
}
Managed is a self-healing connection to one host: it keeps a single live Client, redialing with jittered exponential backoff after failures, and multiplexes all execution over it — only dials pay the handshake.
func (*Managed) Client ¶
Client returns the live connection for direct reuse — an interactive session on an already-pooled host without a second handshake — or nil when not currently ready. The Managed still owns it: don't Close it.
func (*Managed) Close ¶
func (m *Managed) Close()
Close stops maintaining the connection and tears down the live transport. StateClosed is terminal; subsequent operations return ErrClosed.
func (*Managed) CombinedOutput ¶
CombinedOutput runs cmd on the live connection. It returns ErrNotReady immediately — without blocking — when no connection is established, so a caller polling many hosts never stalls on a dead one, and ErrClosed after Close. A transport-level failure schedules a reconnect; a non-zero exit does not.
type ManagedConfig ¶
type ManagedConfig struct {
// Dial establishes the connection; it is called for the first connect and
// every reconnect. Its ctx is canceled when the Managed closes.
Dial func(ctx context.Context) (*Client, error)
// OnStateChange, when non-nil, is invoked on every lifecycle transition
// with the new state and the error that drove it (nil on recovery and on
// close). Transitions from the maintenance loop arrive in order; the
// StateClosed notification comes from the closing goroutine and is not
// ordered relative to a loop notification already in flight — State()
// itself is always accurate after Close. Keep the callback fast and
// non-blocking.
OnStateChange func(s State, err error)
}
ManagedConfig configures one self-healing connection.
type Pool ¶
type Pool struct {
// contains filtered or unexported fields
}
Pool owns a set of Managed connections and the dial-concurrency limit shared across them. Capping concurrent dials keeps a cold start of many hosts from tripping a server's sshd MaxStartups throttle or exhausting local sockets.
func NewPool ¶
NewPool returns a pool permitting at most maxConcurrentDials dials in flight across all its connections. Non-positive means DefaultMaxDials.
func (*Pool) Add ¶
func (p *Pool) Add(cfg ManagedConfig) *Managed
Add registers a connection and starts maintaining it in the background, returning immediately. The returned Managed is usable at once — its execution methods report ErrNotReady until the first dial succeeds.
type Result ¶
type Result struct {
Stdout []byte
Stderr []byte
ExitCode int // -1 when the command never returned a status
}
Result is the outcome of a one-shot command.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is a live interactive shell on the remote host.
func (*Session) Close ¶
Close tears the session down. It is idempotent; every call returns the first close's error.
type SessionConfig ¶
type SessionConfig struct {
Stdin io.Reader
Stdout, Stderr io.Writer
TTY *TTYConfig // nil: no PTY is requested
}
SessionConfig wires an interactive session to the consumer's streams. Any nil stream is simply not connected.
type State ¶
type State int
State is the lifecycle state of a Managed connection.
const ( // StateConnecting is the initial state, before the first dial resolves. StateConnecting State = iota // StateReady means a live connection is established and usable. StateReady // StateBroken means the last attempt failed; a reconnect is scheduled. StateBroken // StateClosed is terminal: the Managed was closed and will not reconnect. StateClosed )
type TTYConfig ¶
type TTYConfig struct {
Term string // DefaultTerm when empty
Cols, Rows int // DefaultCols x DefaultRows when non-positive
}
TTYConfig requests a remote pseudo-terminal. The library never inspects the local environment — the consumer supplies the terminal type and size it wants the remote side to see.
type UnknownHostKeyError ¶
UnknownHostKeyError reports a host absent from known_hosts that was not (or could not be) confirmed.
func (*UnknownHostKeyError) Error ¶
func (e *UnknownHostKeyError) Error() string
Error implements error.